2012 Markana, Inc. All rights reserved.
The purpose of this module is to explain the anatomy of the Android OS. We will walk through the Android stack, starting from the bottom and moving up:
| Android Version | Kernel Version |
|---|---|
1.0 |
2.6.25 |
1.5 |
2.6.27 |
1.6 |
2.6.29 |
2.2 |
2.6.32 |
2.3 |
2.6.35 |
3.0 |
2.6.36 |
4.0.1 |
3.0.1 |
4.0.3 |
3.0.8 |
4.1.1 |
3.0.31 (GN), 3.1.10 (N7) |
|
|
Significant efforts have been made by the Linux community to move most of the Android-specific changes back into the mainline Linux kernel (3.3 and 3.5), with a hope that Android could then switch to the official kernel. |
| Android Version | Kernel Version |
|---|---|
kernel/common.git |
The "official" Android Kernel branch (used as the basis for others) |
kernel/goldfish.git |
Kernel tree for the "goldfish" emulator |
kernel/experimental.git |
The experimental extensions |
kernel/linux-2.6.git |
The mirror of Linus' kernel tree |
kernel/lk.git |
(L)ittle (K)ernel bootloader |
kernel/msm.git |
Kernel tree for MSM7XXX family and Android on MSM7XXX (Qualcomm) |
kernel/omap.git |
Kernel tree for TI’s OMAP family SOCs on Android |
kernel/samsung.git |
Kernel tree for Samsung SOCs systems on Android |
kernel/tegra.git |
Kernel tree for NVIDIA Tegra family SOCs on Android |
The following are some of the changes/additions Android makes to the Linux kernel.
int size = 4096; int fd = ashmem_create_region("MySharedRegionName", size); if (fd == 0) { data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if(data != MAP_FAILED) { /* for security reasons, no other process can ashmem_create_region() with the same name */ /* instead, to share this memory, we send fd via Binder IPC to another process */ /* that process then mmap()'s it the same way in order to access the shared memory */ } }
#include <linux/wakelock.h> wake_lock_init(struct wakelock *lock, int type, const char *name); void wake_lock(struct wake_lock *lock); void wake_unlock(struct wake_lock *lock); void wake_lock_timeout(struct wake_lock *lock, long timeout); void wake_lock_destroy(struct wake_lock *lock); int wake_lock_active(struct wake_lock *lock); long has_wake_lock(int type);
echo "MyLock" > /sys/power/wake_lock echo "MyLock" > /sys/power/wake_unlock
| Flag | CPU | Screen | Keybaord |
|---|---|---|---|
PARTIAL_WAKE_LOCK |
On |
Off |
Off |
SCREEN_DIM_WAKE_LOCK |
On |
Dim |
Off |
SCREEN_BRIGHT_WAKE_LOCK |
On |
Bright |
Off |
FULL_WAKE_LOCK |
On |
Bright |
On |
| Flag | Notes |
|---|---|
ACQUIRE_CAUSES_WAKEUP |
Force screen to turn on as soon as the wake-lock is acquired (i.e. don’t wait for user activity) |
ON_AFTER_RELEASE |
When set, the user activity timer will be reset when the wake-lock is released, causing the screen/keyboard illumination to remain on a bit longer (reduces flicker if code is cycling between wake-locks) |
# Define the oom_adj values for the classes of processes that can be
# killed by the kernel. These are used in ActivityManagerService.
setprop ro.FOREGROUND_APP_ADJ 0
setprop ro.VISIBLE_APP_ADJ 1
setprop ro.PERCEPTIBLE_APP_ADJ 2
setprop ro.HEAVY_WEIGHT_APP_ADJ 3
setprop ro.SECONDARY_SERVER_ADJ 4
setprop ro.BACKUP_APP_ADJ 5
setprop ro.HOME_APP_ADJ 6
setprop ro.HIDDEN_APP_MIN_ADJ 7
setprop ro.EMPTY_APP_ADJ 15
# Define the memory thresholds at which the above process classes will
# be killed. These numbers are in pages (4k).
setprop ro.FOREGROUND_APP_MEM 2048
setprop ro.VISIBLE_APP_MEM 3072
setprop ro.PERCEPTIBLE_APP_MEM 4096
setprop ro.HEAVY_WEIGHT_APP_MEM 4096
setprop ro.SECONDARY_SERVER_MEM 6144
setprop ro.BACKUP_APP_MEM 6144
setprop ro.HOME_APP_MEM 6144
setprop ro.HIDDEN_APP_MEM 7168
setprop ro.EMPTY_APP_MEM 8192
# Write value must be consistent with the above properties.
# Note that the driver only supports 6 slots, so we have combined some of
# the classes into the same memory level; the associated processes of higher
# classes will still be killed first.
write /sys/module/lowmemorykiller/parameters/adj 0,1,2,4,7,15
write /proc/sys/vm/overcommit_memory 1
write /proc/sys/vm/min_free_order_shift 4
write /sys/module/lowmemorykiller/parameters/minfree 2048,3072,4096,6144,7168,8192
# Set init its forked children's oom_adj.
write /proc/1/oom_adj -16
Foreground processes - with an Activity that just ran onResume(), or a Service bound to it or started as foreground, or executing its callback methods, or a BroadcastReceiver executing onReceive()
Visible processes - with an Activity that just ran onPause() but is still visible or a Service bound to a component from a visible process
Service processes - with a Service that has been started with Context.startService()
Background processes - with an Activity that just ran onStop()
Empty processes - with no components (kept around just for caching purposes)
... #define AID_NET_BT_ADMIN 3001 #define AID_NET_BT 3002 #define AID_INET 3003 ...
... #include <linux/android_aid.h> static inline int current_has_network(void) { return in_egroup_p(AID_INET) || capable(CAP_NET_RAW); } ... static int inet_create(struct net *net, struct socket *sock, int protocol) { ... if (!current_has_network()) return -EACCES; ... } ...
... #define AID_NET_BT_ADMIN 3001 /* bluetooth: create any socket */ #define AID_NET_BT 3002 /* bluetooth: create sco, rfcomm or l2cap sockets */ #define AID_INET 3003 /* can create AF_INET and AF_INET6 sockets */ ... static const struct android_id_info android_ids[] = { ... { "net_bt_admin", AID_NET_BT_ADMIN, }, { "net_bt", AID_NET_BT, }, ... { "inet", AID_INET, }, ... }; ...
<permissions> ... <permission name="android.permission.BLUETOOTH_ADMIN" > <group gid="net_bt_admin" /> </permission> <permission name="android.permission.BLUETOOTH" > <group gid="net_bt" /> </permission> <permission name="android.permission.INTERNET" > <group gid="inet" /> </permission> ... </permissions>
write /proc/sys/kernel/panic_on_oops 1 write /proc/sys/kernel/hung_task_timeout_secs 0 write /proc/cpu/alignment 4 write /proc/sys/kernel/sched_latency_ns 10000000 write /proc/sys/kernel/sched_wakeup_granularity_ns 2000000 write /proc/sys/kernel/sched_compat_yield 1 write /proc/sys/kernel/sched_child_runs_first 0
<*_HARDWARE_MODULE_ID>.<ro.product.board>.so
<*_HARDWARE_MODULE_ID>.<ro.board.platform>.so
<*_HARDWARE_MODULE_ID>.<ro.arch>.so
<*_HARDWARE_MODULE_ID>.default.so
dev_mount sdcard /mnt/sdcard auto /devices/platform/goldfish_mmc.0 /devices/platform/msm_sdcc.2/mmc_host/mmc1
dev_mount sdcard /mnt/sdcard 3 /devices/platform/s3c-sdhci.0/mmc_host/mmc0/mmc0:0001/block/mmcblk0
... /dev/urandom 0666 root root /dev/ashmem 0666 root root /dev/binder 0666 root root ... /dev/log/* 0662 root log ... /dev/ttyMSM0 0600 bluetooth bluetooth ... /dev/alarm 0664 system radio ... /dev/cam 0660 root camera ... /dev/akm8976_pffd 0640 compass system /dev/lightsensor 0640 system system ... /dev/bus/usb/* 0660 root usb /sys/devices/virtual/input/input* enable 0660 root input ...
What are the four layers of the Android Stack?
What is the purpose of the Linux Kernel? How does Android use it?
What’s missing in Android for it to be considered a more traditional Linux distribution?
How is the Linux kernel on Android different?
Name at least five Android Linux kernel extensions.
What is Binder?
What does Binder do and why do we need it?
How is Binder exposed to the user-space?
How does Binder compare to other similar mechanisms?
What is Ashmem?
Why do we need Ashmem?
How do applications use Ashmem?
How does Ashmem compare to other similar mechanisms?
How does the power management on Android compare to traditional Linux distributions?
What are wake locks?
Where in the stack are the wake locks used?
How do applications get access to wake locks?
What is the purpose of early suspend feature of the Linux kernel?
What is the purpose of the alarm driver?
What are the two opposing objectives on Android when it comes with memory management?
What is Android’s first line of defense when it comes to memory management?
What is the low-memory-killer and what does it do?
What are the relative levels of priority of application processes on Android?
How does the low-memory-killer know who to kill and when?
How can applications avoid being low-memory-killed?
What is the purpose of the logger on Android?
Name at least three log destinations.
Where is the log information stored?
What is the purpose of the CONFIG_ANDROID_PARANOID_NETWORK kernel option on Android?
Name at least three sub-layers of the native layer.
What is Bionic, why do we need it, and how does it differ from its alternatives?
What are the two main purposes of user-space HAL on Android?
How is the user-space HAL on Android exposed to the layers above it?
Name at least three classes of devices exposed by user-space HAL.
Name at least five native daemons and explain what they do.
What is the purpose of ueventd on Android?
What are the two "flingers" on Android?
Name at least three "function" libraries on Android.
What is the name of the Android’s media framework?
What is the purpose of Dalvik on Android?
How does Dalvik differ from its alternatives?
What is the purpose of zygote?
What was added to Dalvik in Froyo (Android 2.2)?
What’s inside the Android Application Framework layer?
What is the purpose of system services on Android? What value do they add?
What is the purpose of managers for system services?
What is the purpose of servicemanager daemon?
Name at least five system services on Android.
What does ActivityManagerService do?
What does PackageManagerService do?
What does the PowerManagerService do?
What does the AlarmManagerService do?
What does the KeyguardManagerService do?
What does the InputMethodManagerService do?
What is Android CDD and why do we need it?
What’s inside an APK?
What’s the difference between system and non-system apps?
What are the different classes of applications that ship with Android?
Name at least three "special" applications on Android?
package com.marakana.android.fetchurl; public class FetchUrl { public static native byte[] fetch(String url); static { System.loadLibrary("fetchurl"); } }
In this module, we’ll explore the following topics:
|
|
JNI can also be used to invoke Java code from within natively-written applications - such as those written in C/C++. In fact, the java command-line utility is an example of one such application, that launches Java code in a Java Virtual Machine. |
We start by creating a Java class with one or more native methods
package com.marakana.jniexamples; public class Hello { public static native void sayHi(String who, int times); //static { System.loadLibrary("hello"); //
} public static void main(String[] args) { sayHi(args[0], Integer.parseInt(args[1])); //
} }
| The method sayHi(String, int) will be implemented in C/C+ in separate files, which will be compiled into a shared library. | |
| Load the shared library by its logical name. The actual name is system-dependent: libhello.so (on Linux/Unix), hello.dll (on Windows), and libhello.jnilib (Mac OSX). | |
| Here we call our native method as a regular Java method. |
Compile the Java code
$ mkdir -p bin $ javac -d bin/ src/com/marakana/jniexamples/Hello.java
Using the javah tool, we generate the C header file from the compiled com.marakana.jniexamples.Hello class:
$ mkdir -p jni $ javah -jni -classpath bin -d jni com.marakana.jniexamples.Hello
Observe the generated C header file:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_marakana_jniexamples_Hello */ #ifndef _Included_com_marakana_jniexamples_Hello #define _Included_com_marakana_jniexamples_Hello #ifdef __cplusplus extern "C" { #endif /* * Class: com_marakana_jniexamples_Hello * Method: sayHi * Signature: (Ljava/lang/String;I)V */ JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi (JNIEnv *, jclass, jstring, jint); #ifdef __cplusplus } #endif #endif
|
|
Method names resolve to C functions based on a pre-defined naming strategy: the prefix Java_, followed by a mangled fully-qualified class name, followed by an underscore ("_") separator, followed by a mangled method name. For overloaded native methods, two underscores ("__") followed by the mangled argument signature. |
Provide the C implementation:
#include "com_marakana_jniexamples_Hello.h" JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi (JNIEnv *env, jclass clazz, jstring who, jint times) { const char *name = (*env)->GetStringUTFChars(env, who, NULL); if (name != NULL) { jint i; for (i = 0; i < times; i++) { printf("Hello %s\n", name); } (*env)->ReleaseStringUTFChars(env, who, name); } }
|
|
Most of the time, we cannot just use Java data types directly in C. For example, we have to convert java.lang.String to char * before we can effectively use it in C. |
|
|
This code assumes: #define NULL ((void *) 0) |
Compile the shared library
$ mkdir -p libs $ gcc -o libs/libhello.jnilib -lc -shared \ -I/System/Library/Frameworks/JavaVM.framework/Headers \ jni/com_marakana_jniexamples_Hello.c $ file libs/libhello.jnilib libs/libhello.jnilib: Mach-O 64-bit dynamically linked shared library x86_64
|
|
On Unix/Linux, compile as: gcc -o libs/libhello.so -lc -shared -fPIC -I$JAVA_HOME/include jni/com_marakana_jniexamples_Hello.c |
Run our code
$ java -Djava.library.path=libs -classpath bin com.marakana.jniexamples.Hello Student 5 Hello Student Hello Student Hello Student Hello Student Hello Student
|
|
Instead of specifying -Djava.library.path=libs, we could have preceded our java command with export LD_LIBRARY_PATH=libs. |
|
|
Common mistakes resulting in java.lang.UnsatisfiedLinkError usually come from incorrect naming of the shared library (O/S-dependent), the library not being in the search path, or wrong library being loaded by Java code. |
| Java Language Type | Native Type | Description | typedef (C99) | typedef (otherwise) |
|---|---|---|---|---|
boolean |
jboolean |
unsigned 8 bits |
uint8_t |
unsigned char |
byte |
jbyte |
signed 8 bits |
int8_t |
signed char |
char |
jchar |
unsigned 16 bits |
uint16_t |
unsigned short |
short |
jshort |
signed 16 bits |
int16_t |
short |
int |
jint |
signed 32 bits |
int32_t |
int |
long |
jlong |
signed 64 bits |
int64_t |
long long |
float |
jfloat |
32-bit IEEE 754 |
float |
float |
double |
jdouble |
64-bit IEEE 754 |
double |
double |
void |
void |
N/A |
N/A |
N/A |
N/A |
jsize |
used to describe cardinal indices and sizes |
jint |
jint |
| Java Boolean Value | Native Boolean Type | Definition |
|---|---|---|
false |
JNI_FALSE |
#define JNI_FALSE 0 |
true |
JNI_TRUE |
#define JNI_TRUE 1 |
| Java Language Type | Native Type | typedef in C |
|---|---|---|
java.lang.Object |
jobject |
void* |
java.lang.Class |
jclass |
jobject |
java.lang.Throwable |
jthrowable |
jobject |
java.lang.String |
jstring |
jobject |
java.lang.ref.WeakReference |
jweak |
jobject |
N/A |
jarray |
jobject |
java.lang.Object[] |
jobjectArray |
jarray |
boolean[] |
jbooleanArray |
jarray |
byte[] |
jbyteArray |
jarray |
char[] |
jcharArray |
jarray |
short[] |
jshortArray |
jarray |
int[] |
jintArray |
jarray |
long[] |
jlongArray |
jarray |
float[] |
jfloatArray |
jarray |
double[] |
jdoubleArray |
jarray |
|
|
A note about reference types in C++
These reference types in C++ are defined as proper classes: class _jobject {}; class _jclass : public _jobject {}; class _jstring : public _jobject {}; class _jarray : public _jobject {}; class _jobjectArray : public _jarray {}; class _jbooleanArray : public _jarray {}; class _jbyteArray : public _jarray {}; class _jcharArray : public _jarray {}; class _jshortArray : public _jarray {}; class _jintArray : public _jarray {}; class _jlongArray : public _jarray {}; class _jfloatArray : public _jarray {}; class _jdoubleArray : public _jarray {}; class _jthrowable : public _jobject {}; typedef _jobject* jobject; typedef _jclass* jclass; typedef _jstring* jstring; typedef _jarray* jarray; typedef _jobjectArray* jobjectArray; typedef _jbooleanArray* jbooleanArray; typedef _jbyteArray* jbyteArray; typedef _jcharArray* jcharArray; typedef _jshortArray* jshortArray; typedef _jintArray* jintArray; typedef _jlongArray* jlongArray; typedef _jfloatArray* jfloatArray; typedef _jdoubleArray* jdoubleArray; typedef _jthrowable* jthrowable; typedef _jobject* jweak; |
|
|
A tip about NULL in Android NDK
Android’s native development kit (NDK) does not define NULL in its jni.h, so the following definition can be useful when working with pointers and reference types: #define NULL ((void *) 0) |
|
|
Opaque references are C pointer types that refer to internal data structures in the JVM. It is an error to try to dereference opaque references and try to use them directly. |
void DeleteLocalRef(JNIEnv *env, jobject localRef);
|
|
In practice, we only really want to use DeleteLocalRef(JNIEnv *, jobject) if we know we are not going to need the reference in the rest of the function body, say before we start executing code that may take a long time. This allows the referenced memory to be freed by GC, assuming nobody else is using it. |
jint EnsureLocalCapacity(JNIEnv *env, jint capacity); jint PushLocalFrame(JNIEnv *env, jint capacity); jobject PopLocalFrame(JNIEnv *env, jobject result);
jobject NewGlobalRef(JNIEnv *env, jobject obj);
void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
jweak NewWeakGlobalRef(JNIEnv *env, jobject obj); void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
/* On Unicode (UTF-16) Characters */ jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize len); jsize GetStringLength(JNIEnv *env, jstring string); const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy); void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars); void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize len, jchar *buf); /* On (modified) UTF-8 Characters */ jstring NewStringUTF(JNIEnv *env, const char *bytes); jsize GetStringUTFLength(JNIEnv *env, jstring string); const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy); void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf); void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize len, char *buf);
|
|
A note about GetString[UTF]Chars(…) functions
The pointer resulting from GetString[UTF]Chars(…) is valid until ReleaseString[UTF]Chars(…) is called. If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made. When *isCopy == JNI_FALSE, the returned string is a direct pointer to the characters in the original java.lang.String instance, which is then pinned in memory. The native code must ensure not to modify the contents of the returned string, otherwise, it would be modifying the private data of the immutable java.lang.String object! Regardless of isCopy, we have to call ReleaseString[UTF]Chars(…) when we are done using the character array, either to free the memory (when *isCopy == JNI_TRUE) or to un-pin the original string in memory (when *isCopy == JNI_FALSE). |
|
|
A note about modified UTF-8 strings:
JNI’s UTF string functions (that work with char *) return/assume \0-terminated character arrays that are encoded as UTF-8 character sequences, except that if the string contains a \u0000 character, it is represented by a pair of bytes 0xc0 0x80 (1100000010000000) instead of 0x00. When working with regular ASCII characters, each character is represented by exactly one byte. |
#include <stdio.h> #include "com_marakana_jniexamples_HelloName.h" JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass clazz, jstring name) { printf("Hello %s", name); /**/ }
| This example would not work (would likely crash the VM) since the jstring type represents strings in the Java virtual machine. This is different from the C string type (char *). |
#include <stdio.h> #include "com_marakana_jniexamples_HelloName.h" JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass clazz, jstring name){ const char *cName = (*env)->GetStringUTFChars(env, name, NULL); /**/ if (cName == NULL) { return; /* OutOfMemoryError already thrown */ } else { printf("Hello %s\n", cName); (*env)->ReleaseStringUTFChars(env, name, cName); /*
*/ } }
| This returns a pointer to an array of bytes representing the string in modified UTF-8 encoding; or NULL if we ran out of memory, in which case java.lang.OutOfMemoryError would also be thrown (upon returning back to the Java runtime). | |
| Calling ReleaseStringUTFChars indicates that the native method no longer needs the UTF-8 string returned by GetStringUTFChars, thus the memory taken by the UTF-8 string can be freed. Failure to do this would result in a memory leak, which could ultimately lead to memory exhaustion. |
#include <stdio.h> #include "com_marakana_jniexamples_GetName.h" JNIEXPORT jstring JNICALL Java_com_marakana_jniexamples_ReturnName_GetName(JNIEnv *env, jclass class) { char buf[20]; fgets(buf, sizeof(buf), stdin); return (*env)->NewStringUTF(env, buf); }
#include <stdio.h> #include "com_marakana_jniexamples_HelloName.h" JNIEXPORT void JNICALL Java_com_marakana_jniexamples_HelloName_sayHelloName(JNIEnv *env, jclass clazz, jstring name) { char buf[4]; jint i; jsize len = (*env)->GetStringUTFLength(env, name); fputs("Hello ", stdout); for (i = 0; i < len; i++) { (*env)->GetStringUTFRegion(env, name, i, 1, buf); putc(buf[0], stdout); /* assumes ASCII */ } putc('\n', stdout); }
jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement); jbooleanArray NewBooleanArray(JNIEnv *env, jsize length); jbyteArray NewByteArray(JNIEnv *env, jsize length); jcharArray NewCharArray(JNIEnv *env, jsize length); jshortArray NewShortArray(JNIEnv *env, jsize length); jintArray NewIntArray(JNIEnv *env, jsize length); jlongArray NewLongArray(JNIEnv *env, jsize length); jfloatArray NewFloatArray(JNIEnv *env, jsize length); jdoubleArray NewDoubleArray(JNIEnv *env, jsize length);
jsize GetArrayLength(JNIEnv *env, jarray array);
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index); void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
jboolean* GetBooleanArrayElements(JNIEnv *env, jbooleanArray array, jboolean *isCopy); jbyte* GetByteArrayElements(JNIEnv *env, jbyteArray array, jboolean *isCopy); jchar* GetCharArrayElements(JNIEnv *env, jcharArray array, jboolean *isCopy); jshort* GetShortArrayElements(JNIEnv *env, jshortArray array, jboolean *isCopy); jint* GetIntArrayElements(JNIEnv *env, jintArray array, jboolean *isCopy); jlong* GetLongArrayElements(JNIEnv *env, jlongArray array, jboolean *isCopy); jfloat* GetFloatArrayElements(JNIEnv *env, jfloatArray array, jboolean *isCopy); jdouble* GetDoubleArrayElements(JNIEnv *env, jdoubleArray array, jboolean *isCopy); void ReleaseBooleanArrayElements(JNIEnv *env, jbooleanArray array, jboolean *elems, jint mode); void ReleaseByteArrayElements(JNIEnv *env, jbyteArray array, jbyte *elems, jint mode); void ReleaseCharArrayElements(JNIEnv *env, jcharArray array, jchar *elems, jint mode); void ReleaseShortArrayElements(JNIEnv *env, jshortArray array, jshort *elems, jint mode); void ReleaseIntArrayElements(JNIEnv *env, jintArray array, jint *elems, jint mode); void ReleaseLongArrayElements(JNIEnv *env, jlongArray array, jlong *elems, jint mode); void ReleaseFloatArrayElements(JNIEnv *env, jfloatArray array, jfloat *elems, jint mode); void ReleaseDoubleArrayElements(JNIEnv *env, jdoubleArray array, jdouble *elems, jint mode);
|
|
A note about memory
The pointer resulting from GetTypeArrayElements(…) is valid until ReleaseTypeArrayElements(…) is called (unless mode == JNI_COMMIT). If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made. When *isCopy == JNI_FALSE, the returned array is a direct pointer to the elements of the Java array, which is then pinned in memory. Regardless of isCopy, we have to call ReleaseTypeArrayElements(…, int mode) when we are done using the native array, either to un-pin the Java array in memory when *isCopy == JNI_FALSE, or, when *isCopy == JNI_TRUE, to:
JNI also supports a critical version of these functions: void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy); void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode); The function GetPrimitiveArrayCritical(…) is similar to GetTypeArrayElements(…), except that the VM is more likely to return the pointer to the primitive array (i.e. *isCopy == JNI_FALSE is more likely). However, this comes with some caveats. The native code between GetPrimitiveArrayCritical(…) and ReleasePrimitiveArrayCritical(…) must not call other JNI functions, or make any system calls that may cause the current thread to block and wait for another Java thread. This is because the VM may temporarily suspend the garbage collection - in case it does not support memory pinning. |
void GetBooleanArrayRegion(JNIEnv *env, jbooleanArray array, jsize start, jsize len, jboolean *buf); void GetByteArrayRegion(JNIEnv *env, jbyteArray array, jsize start, jsize len, jbyte *buf); void GetCharArrayRegion(JNIEnv *env, jcharArray array, jsize start, jsize len, jchar *buf); void GetShortArrayRegion(JNIEnv *env, jshortArray array, jsize start, jsize len, jshort *buf); void GetIntArrayRegion(JNIEnv *env, jintArray array, jsize start, jsize len, jint *buf); void GetLongArrayRegion(JNIEnv *env, jlongArray array, jsize start, jsize len, jlong *buf); void GetFloatArrayRegion(JNIEnv *env, jfloatArray array, jsize start, jsize len, jfloat *buf); void GetDoubleArrayRegion(JNIEnv *env, jdoubleArray array, jsize start, jsize len, jdouble *buf); void SetBooleanArrayRegion(JNIEnv *env, jbooleanArray array, jsize start, jsize len, jboolean *buf); void SetByteArrayRegion(JNIEnv *env, jbyteArray array, jsize start, jsize len, jbyte *buf); void SetCharArrayRegion(JNIEnv *env, jcharArray array, jsize start, jsize len, jchar *buf); void SetShortArrayRegion(JNIEnv *env, jshortArray array, jsize start, jsize len, jshort *buf); void SetIntArrayRegion(JNIEnv *env, jintArray array, jsize start, jsize len, jint *buf); void SetLongArrayRegion(JNIEnv *env, jlongArray array, jsize start, jsize len, jlong *buf); void SetFloatArrayRegion(JNIEnv *env, jfloatArray array, jsize start, jsize len, jfloat *buf); void SetDoubleArrayRegion(JNIEnv *env, jdoubleArray array, jsize start, jsize len, jdouble *buf);
JNIEXPORT jint JNICALL Java_com_marakana_jniexamples_Math_sum(JNIEnv *env, jclass clazz, jintArray array) { jinit *cArray = (*env)->GetIntArrayElements(env, array, NULL); if (cArray == NULL) { return 0; } else { jini len = (*env)->GetArrayLength(env, array); jint i; jint result = 0; for (i = 0; i < len; i++) { result += cArray[i]; } (*env)->ReleaseIntArrayElements(env, array, cArray, JNI_ABORT); return result; } }
JNIEXPORT jint JNICALL Java_com_marakana_jniexamples_Math_sum(JNIEnv *env, jclass clazz, jintArray array) { jint buf[1]; jini len = (*env)->GetArrayLength(env, array); jint i; jint result = 0; for (i = 0; i < len; i++) { (*env)->GetIntArrayRegion(env, array, i, 1, buf); result += buf[0]; } return result; }
JNIEXPORT jintArray JNICALL Java_com_marakana_jniexamples_Foo_getData(JNIEnv *env, jclass class) { jint cArray[10]; jsize len = sizeof(cArray); jintArray jArray = (*env)->NewIntArray(env, len); if (jArray != NULL) { jint i; /* "get" the data */ for (i = 0; i < len; i++) { cArray[i] = i; } (*env)->SetIntArrayRegion(env, jArray, 0, len, cArray); } return jArray; }
jobject NewDirectByteBuffer(JNIEnv* env, void* address, jlong capacity); void* GetDirectBufferAddress(JNIEnv* env, jobject buf); jlong GetDirectBufferCapacity(JNIEnv* env, jobject buf);
public class Foo { public static void main(String[] args) { … ByteBuffer buf = ByteBuffer.allocateDirect(1024); // populate buf processData(buf); … } public native static void processData(ByteBuffer buf); }
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Foo_processData(JNIEnv *env, jclass clazz, jobject buf) { char *cBuf = (*env)->GetDirectBufferAddress(env, buf); /* process cBuf from 0 to (*env)->GetDirectBufferCapacity(env, buf) */ }
jclass FindClass(JNIEnv *env, const char *name);
/* load the java.lang.String class */ (*env)->FindClass(env, "java/lang/String");
/* load the java.lang.String[] class */ (*env)->FindClass(env, "[Ljava/lang/String;");
jclass GetSuperclass(JNIEnv *env, jclass clazz); jboolean IsAssignableFrom(JNIEnv *env, jclass clazz1, jclass clazz2);
jclass GetObjectClass(JNIEnv *env, jobject obj); jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz);
| Type Signature | Java Type |
|---|---|
Z |
boolean |
B |
byte |
C |
char |
S |
short |
I |
int |
J |
long |
F |
float |
D |
double |
Lfully-qualified-class; |
fully-qualified-class |
[type |
type[] |
(arg-types)ret-type |
method type |
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig); jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jobject (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID); jboolean (*GetStaticBooleanField)(JNIEnv*, jclass, jfieldID); jbyte (*GetStaticByteField)(JNIEnv*, jclass, jfieldID); jchar (*GetStaticCharField)(JNIEnv*, jclass, jfieldID); jshort (*GetStaticShortField)(JNIEnv*, jclass, jfieldID); jint (*GetStaticIntField)(JNIEnv*, jclass, jfieldID); jlong (*GetStaticLongField)(JNIEnv*, jclass, jfieldID); jfloat (*GetStaticFloatField)(JNIEnv*, jclass, jfieldID); jdouble (*GetStaticDoubleField)(JNIEnv*, jclass, jfieldID); void (*SetStaticObjectField)(JNIEnv*, jclass, jfieldID, jobject); void (*SetStaticBooleanField)(JNIEnv*, jclass, jfieldID, jboolean); void (*SetStaticByteField)(JNIEnv*, jclass, jfieldID, jbyte); void (*SetStaticCharField)(JNIEnv*, jclass, jfieldID, jchar); void (*SetStaticShortField)(JNIEnv*, jclass, jfieldID, jshort); void (*SetStaticIntField)(JNIEnv*, jclass, jfieldID, jint); void (*SetStaticLongField)(JNIEnv*, jclass, jfieldID, jlong); void (*SetStaticFloatField)(JNIEnv*, jclass, jfieldID, jfloat); void (*SetStaticDoubleField)(JNIEnv*, jclass, jfieldID, jdouble); jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID); jboolean (*GetBooleanField)(JNIEnv*, jobject, jfieldID); jbyte (*GetByteField)(JNIEnv*, jobject, jfieldID); jchar (*GetCharField)(JNIEnv*, jobject, jfieldID); jshort (*GetShortField)(JNIEnv*, jobject, jfieldID); jint (*GetIntField)(JNIEnv*, jobject, jfieldID); jlong (*GetLongField)(JNIEnv*, jobject, jfieldID); jfloat (*GetFloatField)(JNIEnv*, jobject, jfieldID); jdouble (*GetDoubleField)(JNIEnv*, jobject, jfieldID); void (*SetObjectField)(JNIEnv*, jobject, jfieldID, jobject); void (*SetBooleanField)(JNIEnv*, jobject, jfieldID, jboolean); void (*SetByteField)(JNIEnv*, jobject, jfieldID, jbyte); void (*SetCharField)(JNIEnv*, jobject, jfieldID, jchar); void (*SetShortField)(JNIEnv*, jobject, jfieldID, jshort); void (*SetIntField)(JNIEnv*, jobject, jfieldID, jint); void (*SetLongField)(JNIEnv*, jobject, jfieldID, jlong); void (*SetFloatField)(JNIEnv*, jobject, jfieldID, jfloat); void (*SetDoubleField)(JNIEnv*, jobject, jfieldID, jdouble);
public class Foo { private String bar; … public native void processBar(); }
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Foo_processBar(JNIEnv *env, jobject object) { /* Same as object.getClass() */ jclass clazz = (*env)->GetObjectClass(env, object); if (clazz != NULL) { /* cannot be null in this case */ /* Same as clazz.getField("bar") */ jfieldID field = (*env)->GetFieldID(env, clazz, "bar", "Ljava/lang/String;"); if (field != NULL) { /* make sure we got the field */ /* Same as field.get(object) */ jstring jString = (*env)->GetObjectField(env, object, field); if (jString != NULL) { /* Convert the value to a C (UTF-8) string */ const char *cString = (*env)->GetStringUTFChars(env, jString, NULL); if (cString == NULL) { return; /* Out of memory */ } printf("Value of \"bar\" before the change: \"%s\"\n", cString); (*env)->ReleaseStringUTFChars(env, jString, cString); } /* Create a new String */ jString = (*env)->NewStringUTF(env, "Bar2"); if (jString != NULL) { /* make sure we are not out of memory */ /* Same as field.set(object, jString) */ (*env)->SetObjectField(env, object, field, jString); } } } }
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jobject (*CallStaticObjectMethod)(JNIEnv*, jclass, jmethodID, ...); jboolean (*CallStaticBooleanMethod)(JNIEnv*, jclass, jmethodID, ...); jbyte (*CallStaticByteMethod)(JNIEnv*, jclass, jmethodID, ...); jchar (*CallStaticCharMethod)(JNIEnv*, jclass, jmethodID, ...); jshort (*CallStaticShortMethod)(JNIEnv*, jclass, jmethodID, ...); jint (*CallStaticIntMethod)(JNIEnv*, jclass, jmethodID, ...); jlong (*CallStaticLongMethod)(JNIEnv*, jclass, jmethodID, ...); jfloat (*CallStaticFloatMethod)(JNIEnv*, jclass, jmethodID, ...); jdouble (*CallStaticDoubleMethod)(JNIEnv*, jclass, jmethodID, ...); void (*CallStaticVoidMethod)(JNIEnv*, jclass, jmethodID, ...); jobject (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...); jboolean (*CallBooleanMethod)(JNIEnv*, jobject, jmethodID, ...); jbyte (*CallByteMethod)(JNIEnv*, jobject, jmethodID, ...); jchar (*CallCharMethod)(JNIEnv*, jobject, jmethodID, ...); jshort (*CallShortMethod)(JNIEnv*, jobject, jmethodID, ...); jint (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...); jlong (*CallLongMethod)(JNIEnv*, jobject, jmethodID, ...); jfloat (*CallFloatMethod)(JNIEnv*, jobject, jmethodID, ...); jdouble (*CallDoubleMethod)(JNIEnv*, jobject, jmethodID, ...); void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
|
|
JNI also offers Call…(…) functions that take args in a form of va_list as well as an array of jvalue-s: <Type> (*Call<Type>MethodV)(JNIEnv*, jobject, jmethodID, va_list); <Type> (*CallStatic<Type>MethodV)(JNIEnv*, jobject, jmethodID, va_list); <Type> (*Call<Type>MethodA)(JNIEnv*, jobject, jmethodID, jvalue*); <Type> (*CallStatic<Type>MethodA)(JNIEnv*, jobject, jmethodID, jvalue*); |
public class Foo { private String bar; public void setBar(String bar) { this.bar = bar; } … public native void processBar(); }
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Foo_processBar(JNIEnv *env, jobject object) { /* Same as object.getClass() */ jclass clazz = (*env)->GetObjectClass(env, object); if (clazz != NULL) { /* Same as clazz.getMethod("setBar", String.class) - assuming non-static */ jmethodID method = (*env)->GetMethodID(env, clazz, "setBar", "(Ljava/lang/String;)V"); if (method != NULL) { /* make sure we found the method */ /* Create a new Java String */ jstring jString = (*env)->NewStringUTF(env, "Bar2"); if (jString != null) { /* Same as method.invoke(object, jString) */ (*env)->CallVoidMethod(env, object, method, jString); } } } }
jint Throw(JNIEnv *env, jthrowable obj);
jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);
static void ThrowExceptionByClassName(JNIEnv *env, const char *name, const char *message) { jclass clazz = (*env)->FindClass(env, name); if (clazz != NULL) { (*env)->ThrowNew(env, clazz, message); (*env)->DeleteLocalRef(env, clazz); } } … if (invalidArgument == TRUE) { ThrowExceptionByClassName(env, "java/lang/IllegalArgumentException", "This argument is not valid!"); }
jthrowable ExceptionOccurred(JNIEnv *env); /* NULL if no exception is currently being thrown */ jboolean ExceptionCheck(JNIEnv *env); void ExceptionDescribe(JNIEnv *env); void ExceptionClear(JNIEnv *env);
… (*env)->CallObjectMethod(env, …); /* this can throw an exception */ if ((*env)->ExceptionCheck(env)) { jthrowable throwable = (*env)->ExceptionOccurred(env); (*env)->ExceptionDescribe(env); /* optionally dump the stack trace */ (*env)->ExceptionClear(env); /* mark the exception as "handled" */ jclazz clazz = (*env)->GetObjectClass(env, throwable); jmethodID getMessageMethod = (*env)->GetMethodID(env, clazz, "getMessage", "()Ljava/lang/String;"); jstring message = (*env)->CallObjectMethod(env, throwable, getMessageMethod); const char *cMessage = (*env)->GetStringUTFChars(env, message, NULL); if (cMessage) { printf("ERROR: %s\n", cMessage); (*env)->ReleaseStringUTFChars(env, message, cMessage); } (*env)->DeleteLocalRef(env, clazz); } …
We start off by defining C function prototypes as native Java methods (wrapped in some class):
package com.marakana.android.fibonaccinative; import android.util.Log; public class FibLib { private static final String TAG = "FibLib"; private static long fib(long n) { return n <= 0 ? 0 : n == 1 ? 1 : fib(n - 1) + fib(n - 2); } // Recursive Java implementation of the Fibonacci algorithm // (included for comparison only) public static long fibJR(long n) { Log.d(TAG, "fibJR(" + n + ")"); return fib(n); } // Function prototype for future native recursive implementation // of the Fibonacci algorithm public native static long fibNR(long n); // Iterative Java implementation of the Fibonacci algorithm // (included for comparison only) public static long fibJI(long n) { Log.d(TAG, "fibJI(" + n + ")"); long previous = -1; long result = 1; for (long i = 0; i <= n; i++) { long sum = result + previous; previous = result; result = sum; } return result; } // Function prototype for future iterative recursive implementation // of the Fibonacci algorithm public native static long fibNI(long n); static { // as defined by LOCAL_MODULE in Android.mk System.loadLibrary("com_marakana_android_fibonaccinative_FibLib"); } }
We then extract our C header file with our function prototypes:
On the command line, change to your project’s root directory
$ cd /path/to/workspace/FibonacciNative
Create jni sub-directory
$ mkdir jni
Extract the C header file from com.marakana.android.fibonaccinative.FibLib class:
$ javah -jni -classpath bin/classes -d jni com.marakana.android.fibonaccinative.FibLib
|
|
Prior to ADT r14, compiled class files were kept directly in the bin/ directory, so in our javah command we would’ve used -classpath bin instead. |
Check out the resulting file:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_marakana_android_fibonaccinative_FibLib */ #ifndef _Included_com_marakana_android_fibonaccinative_FibLib #define _Included_com_marakana_android_fibonaccinative_FibLib #ifdef __cplusplus extern "C" { #endif /* * Class: com_marakana_android_fibonaccinative_FibLib * Method: fibNR * Signature: (J)J */ JNIEXPORT jlong JNICALL Java_com_marakana_android_fibonaccinative_FibLib_fibNR (JNIEnv *, jclass, jlong); /* * Class: com_marakana_android_fibonaccinative_FibLib * Method: fibNI * Signature: (J)J */ JNIEXPORT jlong JNICALL Java_com_marakana_android_fibonaccinative_FibLib_fibNI (JNIEnv *, jclass, jlong); #ifdef __cplusplus } #endif #endif
|
|
The function prototype names are name-spaced to the classname they are found in. |
We provide the C implementation of com_marakana_android_fibonacci_FibLib.h header file:
/* Include the header file that was created via "javah -jni" command */ #include "com_marakana_android_fibonaccinative_FibLib.h" #include <android/log.h> /* Recursive implementation of the fibonacci algorithm (in a helper function) */ static jlong fib(jlong n) { return n <= 0 ? 0 : n == 1 ? 1 : fib(n - 1) + fib(n - 2); } /* Actual implementation of JNI-defined `fibNR` (recursive) function */ JNIEXPORT jlong JNICALL Java_com_marakana_android_fibonaccinative_FibLib_fibNR (JNIEnv *env, jclass clazz, jlong n) { __android_log_print(ANDROID_LOG_DEBUG, "FibLib.c", "fibNR(%lld)", n); return fib(n); } /* Actual implementation of JNI-defined `fibNI` (iterative) function */ JNIEXPORT jlong JNICALL Java_com_marakana_android_fibonaccinative_FibLib_fibNI (JNIEnv *env, jclass clazz, jlong n) { jlong previous = -1; jlong result = 1; jlong i; __android_log_print(ANDROID_LOG_DEBUG, "FibLib.c", "fibNI(%lld)", n); for (i = 0; i <= n; i++) { jlong sum = result + previous; previous = result; result = sum; } return result; }
We could also use an alternative mechanism of linking native-code to managed code by pre-registering our functions. This leads to earlier detection of method-function mismatch issues, a slight performance improvement, and spares us the redundancy of the header file and the use of the javah command.
#include <jni.h> #include <android/log.h> namespace com_marakana_android_fibonaccinative { static jlong fib(jlong n) { return n <= 0 ? 0 : n == 1 ? 1 : fib(n - 1) + fib(n - 2); } static jlong fibNR(JNIEnv *env, jclass clazz, jlong n) { __android_log_print(ANDROID_LOG_DEBUG, "FibLib.c", "fibNR(%lld)", n); return fib(n); } static jlong fibNI(JNIEnv *env, jclass clazz, jlong n) { jlong previous = -1; jlong result = 1; jlong i; __android_log_print(ANDROID_LOG_DEBUG, "FibLib.c", "fibNI(%lld)", n); for (i = 0; i <= n; i++) { jlong sum = result + previous; previous = result; result = sum; } return result; } static JNINativeMethod method_table[] = { { "fibNR", "(J)J", (void *) fibNR }, { "fibNI", "(J)J", (void *) fibNI } }; } using namespace com_marakana_android_fibonaccinative; extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } else { jclass clazz = env->FindClass("com/marakana/android/fibonaccinative/FibLib"); if (clazz) { jint ret = env->RegisterNatives(clazz, method_table, sizeof(method_table) / sizeof(method_table[0])); env->DeleteLocalRef(clazz); return ret == 0 ? JNI_VERSION_1_6 : JNI_ERR; } else { return JNI_ERR; } } }
|
|
Most of the Android’s JNI-based shared libraries are built using this, "alternative", approach where the functions are pre-registered. |
We need a Android.mk makefile, which will be used by NDK to compile our JNI code into a shared library:
# Defines the root to all other relative paths # The macro function my-dir, provided by the build system, # specifies the path of the current directory (i.e. the # directory containing the Android.mk file itself) LOCAL_PATH := $(call my-dir) # Clear all LOCAL_XXX variables with the exception of # LOCAL_PATH (this is needed because all variables are global) include $(CLEAR_VARS) # List all of our C files to be compiled (header file # dependencies are automatically computed) LOCAL_SRC_FILES := com_marakana_android_fibonaccinative_FibLib.c # The name of our shared module (this name will be prepended # by lib and postfixed by .so) LOCAL_MODULE := com_marakana_android_fibonaccinative_FibLib # We need to tell the linker about our use of the liblog.so LOCAL_LDLIBS += -llog # Collects all LOCAL_XXX variables since "include $(CLEAR_VARS)" # and determines what to build (in this case a shared library) include $(BUILD_SHARED_LIBRARY)
|
|
It’s easiest to copy the Android.mk file from another (sample) project and adjust LOCAL_SRC_FILES and LOCAL_MODULE as necessary |
|
|
See /path/to/ndk-installation-dir/docs/ANDROID-MK.html for the complete reference of Android make files (build system) |
Finally, from the root of our project (i.e. FibonacciNative/), we run ndk-build to build our code into a shared library (FibonacciNative/libs/armeabi/libcom_marakana_android_fibonacci_FibLib.so):
$ ndk-build Compile thumb : com_marakana_android_fibonaccinative_FibLib <= com_marakana_android_fibonaccinative_FibLib.c SharedLibrary : libcom_marakana_android_fibonaccinative_FibLib.so Install : libcom_marakana_android_fibonaccinative_FibLib.so => libs/armeabi/libcom_marakana_android_fibonaccinative_FibLib.so
Running ndk-build clean will clean all generated binaries.
|
|
The command ndk-build comes from the NDK’s installation directory (e.g. /path/to/android-ndk-r5b), so it’s easiest if we add this directory to our PATH. |
|
|
The current version of NDK (at least up to r5b) on Windows depends on Cygwin (a Unix-like environment and command-line interface for Microsoft Windows), or specifically "shell" (bash) and "make" (gmake) programs available through Cygwin. To run ndk-build on Windows, we first need to run bash and then execute ndk-build. It is important that both c:\path\to\cygwin\bin and c:\path\to\ndk be defined in our Path. |
ndk-build "APP_ABI=armeabi armeabi-v7a x86"
We can now build the "client" of our library (in this case a simple activity) to use our FibLib library.
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Get Your Fibonacci Numbers Here!</string> <string name="fibJR">fibJR</string> <string name="fibJI">fibJI</string> <string name="fibNR">fibNR</string> <string name="fibNI">fibNI</string> <string name="app_name">FibonacciNative</string> <string name="button">Get Fibonacci Result</string> </resources>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <!-- This is just a simple title ("Get Your Fibonacci Here!") --> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:text="@string/hello" android:textSize="25sp" /> <!-- This is the entry box for our number "n" --> <EditText android:id="@+id/input" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:gravity="right" android:inputType="number" > <requestFocus /> </EditText> <!-- This radio group allows the user to select the fibonacci implementation type --> <RadioGroup android:id="@+id/type" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <RadioButton android:id="@+id/type_fib_jr" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/fibJR" /> <RadioButton android:id="@+id/type_fib_ji" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/fibJI" /> <RadioButton android:id="@+id/type_fib_nr" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/fibNR" /> <RadioButton android:id="@+id/type_fib_ni" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/fibNI" /> </RadioGroup> <!-- This button allows the user to trigger fibonacci calculation --> <Button android:id="@+id/button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/button" /> <!-- This is the output area for the fibonacci result --> <TextView android:id="@+id/output" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:textSize="20sp" /> </LinearLayout>
package com.marakana.android.fibonaccinative; import android.app.Activity; import android.app.ProgressDialog; import android.os.AsyncTask; import android.os.Bundle; import android.os.SystemClock; import android.text.TextUtils; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.TextView; public class FibonacciActivity extends Activity implements OnClickListener { private EditText input; private RadioGroup type; private TextView output; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); this.input = (EditText) super.findViewById(R.id.input); this.type = (RadioGroup) super.findViewById(R.id.type); this.output = (TextView) super.findViewById(R.id.output); Button button = (Button) super.findViewById(R.id.button); button.setOnClickListener(this); } public void onClick(View view) { String s = this.input.getText().toString(); if (TextUtils.isEmpty(s)) { return; } final ProgressDialog dialog = ProgressDialog.show(this, "", "Calculating...", true); final long n = Long.parseLong(s); new AsyncTask<Void, Void, String>() { @Override protected String doInBackground(Void... params) { long result = 0; long t = SystemClock.uptimeMillis(); switch (FibonacciActivity.this.type.getCheckedRadioButtonId()) { case R.id.type_fib_jr: result = FibLib.fibJR(n); break; case R.id.type_fib_ji: result = FibLib.fibJI(n); break; case R.id.type_fib_nr: result = FibLib.fibNR(n); break; case R.id.type_fib_ni: result = FibLib.fibNI(n); break; } t = SystemClock.uptimeMillis() - t; return String.format("fib(%d)=%d in %d ms", n, result, t); } @Override protected void onPostExecute(String result) { dialog.dismiss(); FibonacciActivity.this.output.setText(result); } }.execute(); } }
The header files for NDK stable APIs are available at /path/to/ndk/platforms/<android-platform>/<arch-name>/usr/include.
|
|
With the exception of the libraries listed above, the native system libraries in the Android platform are not considered "stable" and may change in future platform versions. Unless our library is being built for a specific Android ROM, we should only make use of the stable libraries provided by the NDK. |
|
|
All the header files are available under: /path/to/ndk-installation-dir/platforms/android-9/arch-arm/usr/include/ |
|
|
See /path/to/ndk-installation-dir/docs/STABLE-APIS.html for the complete reference of NDK’s stable APIs. |
Create a simple Android application that allows the end-user to log to Android’s logcat via functionality provided by /system/lib/liblog.so (i.e. you cannot use android.util.Log).
For example, this could be your LogLib:
package com.marakana.android.loglib; public class LogLib { public static native void log(int priority, String tag, String msg); /* You'll need to load your library here */ }
The basic scaffolding for your application, along with the UI is already provided:
You just need to import it into Eclipse (as an existing project into your workspace) and then add the JNI components.
To test, look for your message to appear in adb logcat.
|
|
Make sure that you enter something for both the tag and the message when testing your application. |
As a bonus:
|
|
Don’t forget to convert tag and msg strings from the Java format (jstring) to native format (char *) before trying to use them in int __android_log_write(int prio, const char *tag, const char *text). Also, don’t forget to free the native strings before returning from the native method. Finally, don’t forget to tell the linker about your use of the log library. |
The solution is provided:
Who uses JNI on Android and why?
What is the NDK?
What is JNI?
What is the purpose of jni.h?
What is the purpose of javah tool?
What are the first two arguments to all native functions that model Java methods declared as native?
What is jlong and why do we need it (and its cousins)?
How are boolean-s modeled in native code?
How are java.lang.Object-s modeled in native code?
What’s the definition of NULL?
What is the difference between local and global references?
What is the difference between Java strings and C "strings"?
What is the difference between GetStringChars and GetStringUTFChars?
How do modified UTF-8 strings differ from normal UTF-8 strings?
Why do we have to call ReleaseStringChars or ReleaseStringUTFChars when we are done with the C strings?
Name at least five array operations?
What is the purpose of mode attribute in ReleaseByteArrayElements(…, mode) (and its cousins)?
What is the signature of void f(int[] a, String s, long n)?
What happens when an exception is thrown by native code?
What is the significance of extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) function?
What can we do with jint RegisterNatives(JNIEnv*, jclass, const JNINativeMethod*, jint) function?
What does LOCAL_MODULE specify in Android.mk?
How do we control the ABI when compiling NDK code?
Name at least three stable NDK APIs.
package com.example.app; import com.example.app.Bar; interface IFooService { void save(inout Bar bar); Bar getById(int id); void delete(in Bar bar); List<Bar> getAll(); }
package com.example.app; public interface IFooService extends android.os.IInterface { public static abstract class Stub extends android.os.Binder implements com.example.app.IFooService { … public static com.example.app.IFooService asInterface( android.os.IBinder obj) { … } public android.os.IBinder asBinder() { return this; } … } void save(com.example.app.Bar bar) throws android.os.RemoteException; com.example.app.Bar getById(int id) throws android.os.RemoteException; void delete(com.example.app.Bar bar) throws android.os.RemoteException; java.util.List<Bar> getAll() throws android.os.RemoteException; }
|
|
Eclipse ADT automatically calls aidl for each .aidl file that it finds in our src/ directory |
package com.example.app; import android.os.Parcel; import android.os.Parcelable; public class Bar implements Parcelable { private int id; private String data; public Bar(int id, String data) { this.id = id; this.data = data; } // getters and setters omitted … public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(this.id); parcel.writeString(this.data); } public void readFromParcel(Parcel parcel) { this.id = parcel.readInt(); this.data = parcel.readString(); } public static final Parcelable.Creator<Bar> CREATOR = new Parcelable.Creator<Bar>() { public Bar createFromParcel(Parcel parcel) { return new Bar(parcel.readInt(), parcel.readString()); } public Bar[] newArray(int size) { return new Bar[size]; } }; }
|
|
Here, the public void readFromParcel(Parcel) method is not defined by the Parcelable interface. Instead, it would be required here because Bar is considered mutable - i.e. we expect the remote side to be able to change it in void save(inout Bar bar) method. Similarly, public static final Parcelable.Creator<Bar> CREATOR field is also not defined by the Parcelable interface (obviously). It would needed to reconstruct Bar in response to com.example.app.Bar getById(int id) operation. |
package com.example.app; parcelable Bar;
|
|
AIDL-interfaces have to import parcelable custom classes even if they are in the same package. In the case of the previous example, src/com/example/app/IFooService.aidl would have to import com.example.app.Bar; if it makes any references to com.example.app.Bar even though they are in the same package. |
… static int open_driver() { int fd = open("/dev/binder", O_RDWR); if (fd >= 0) { … size_t maxThreads = 15; result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads); … } else { … } return fd; } …
FibonacciCommon library project - to define our AIDL interface as well as custom types for parameters and return values
FibonacciService project - where we implement our AIDL interface and expose it to the clients
FibonacciClient project - where we connect to our AIDL-defined service and use it
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.fibonaccicommon" android:versionCode="1" android:versionName="1.0"> </manifest>
package com.marakana.android.fibonaccicommon; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; interface IFibonacciService { long fibJR(in long n); long fibJI(in long n); long fibNR(in long n); long fibNI(in long n); FibonacciResponse fib(in FibonacciRequest request); }
package com.marakana.android.fibonaccicommon; parcelable FibonacciRequest;
package com.marakana.android.fibonaccicommon; import android.os.Parcel; import android.os.Parcelable; public class FibonacciRequest implements Parcelable { public static enum Type { RECURSIVE_JAVA, ITERATIVE_JAVA, RECURSIVE_NATIVE, ITERATIVE_NATIVE } private final long n; private final Type type; public FibonacciRequest(long n, Type type) { this.n = n; if (type == null) { throw new NullPointerException("Type must not be null"); } this.type = type; } public long getN() { return n; } public Type getType() { return type; } public int describeContents() { return 0; } public void writeToParcel(Parcel parcel, int flags) { parcel.writeLong(this.n); parcel.writeInt(this.type.ordinal()); } public static final Parcelable.Creator<FibonacciRequest> CREATOR = new Parcelable.Creator<FibonacciRequest>() { public FibonacciRequest createFromParcel(Parcel in) { long n = in.readLong(); Type type = Type.values()[in.readInt()]; return new FibonacciRequest(n, type); } public FibonacciRequest[] newArray(int size) { return new FibonacciRequest[size]; } }; }
package com.marakana.android.fibonaccicommon; parcelable FibonacciResponse;
package com.marakana.android.fibonaccicommon; import android.os.Parcel; import android.os.Parcelable; public class FibonacciResponse implements Parcelable { private final long result; private final long timeInMillis; public FibonacciResponse(long result, long timeInMillis) { this.result = result; this.timeInMillis = timeInMillis; } public long getResult() { return result; } public long getTimeInMillis() { return timeInMillis; } public int describeContents() { return 0; } public void writeToParcel(Parcel parcel, int flags) { parcel.writeLong(this.result); parcel.writeLong(this.timeInMillis); } public static final Parcelable.Creator<FibonacciResponse> CREATOR = new Parcelable.Creator<FibonacciResponse>() { public FibonacciResponse createFromParcel(Parcel in) { return new FibonacciResponse(in.readLong(), in.readLong()); } public FibonacciResponse[] newArray(int size) { return new FibonacciResponse[size]; } }; }
package com.marakana.android.fibonaccicommon; public interface IFibonacciService extends android.os.IInterface { public static abstract class Stub extends android.os.Binder implements com.marakana.android.fibonacci.IFibonacciService { … public static com.marakana.android.fibonacci.IFibonacciService asInterface( android.os.IBinder obj) { … } public android.os.IBinder asBinder() { return this; } … } public long fibJR(long n) throws android.os.RemoteException; public long fibJI(long n) throws android.os.RemoteException; public long fibNR(long n) throws android.os.RemoteException; public long fibNI(long n) throws android.os.RemoteException; public com.marakana.android.fibonaccicommon.FibonacciResponse fib( com.marakana.android.fibonaccicommon.FibonacciRequest request) throws android.os.RemoteException; }
package com.marakana.android.fibonacciservice; import android.os.SystemClock; import android.util.Log; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; import com.marakana.android.fibonaccicommon.IFibonacciService; import com.marakana.android.fibonaccinative.FibLib; public class IFibonacciServiceImpl extends IFibonacciService.Stub { private static final String TAG = "IFibonacciServiceImpl"; public long fibJI(long n) { Log.d(TAG, String.format("fibJI(%d)", n)); return FibLib.fibJI(n); } public long fibJR(long n) { Log.d(TAG, String.format("fibJR(%d)", n)); return FibLib.fibJR(n); } public long fibNI(long n) { Log.d(TAG, String.format("fibNI(%d)", n)); return FibLib.fibNI(n); } public long fibNR(long n) { Log.d(TAG, String.format("fibNR(%d)", n)); return FibLib.fibNR(n); } public FibonacciResponse fib(FibonacciRequest request) { Log.d(TAG, String.format("fib(%d, %s)", request.getN(), request.getType())); long timeInMillis = SystemClock.uptimeMillis(); long result; switch (request.getType()) { case ITERATIVE_JAVA: result = this.fibJI(request.getN()); break; case RECURSIVE_JAVA: result = this.fibJR(request.getN()); break; case ITERATIVE_NATIVE: result = this.fibNI(request.getN()); break; case RECURSIVE_NATIVE: result = this.fibNR(request.getN()); break; default: return null; } timeInMillis = SystemClock.uptimeMillis() - timeInMillis; return new FibonacciResponse(result, timeInMillis); } }
package com.marakana.android.fibonacciservice; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.util.Log; public class FibonacciService extends Service { //private static final String TAG = "FibonacciService"; private IFibonacciServiceImpl service; //
@Override public void onCreate() { super.onCreate(); this.service = new IFibonacciServiceImpl(); //
Log.d(TAG, "onCreate()'ed"); //
} @Override public IBinder onBind(Intent intent) { Log.d(TAG, "onBind()'ed"); //
return this.service; //
} @Override public boolean onUnbind(Intent intent) { Log.d(TAG, "onUnbind()'ed"); //
return super.onUnbind(intent); } @Override public void onDestroy() { Log.d(TAG, "onDestroy()'ed"); this.service = null; super.onDestroy(); } }
| We create yet another "service" object by extending from android.app.Service. The purpose of FibonacciService object is to provide access to our Binder-based IFibonacciServiceImpl object. | |
| Here we simply declare a local reference to IFibonacciServiceImpl, which will act as a singleton (i.e. all clients will share a single instance). Since our IFibonacciServiceImpl does not require any special initialization, we could instantiate it at this point, but we choose to delay this until the onCreate() method. | |
| Now we instantiate our IFibonacciServiceImpl that we’ll be providing to our clients (in the onBind(Intent) method). If our IFibonacciServiceImpl required access to the Context (which it doesn’t) we could pass a reference to this (i.e. android.app.Service, which implements android.content.Context) at this point. Many Binder-based services use Context in order to access other platform functionality. | |
| This is where we provide access to our IFibonacciServiceImpl object to our clients. By design, we chose to have only one instance of IFibonacciServiceImpl (so all clients share it) but we could also provide each client with their own instance of IFibonacciServiceImpl. | |
| We just add some logging calls to make it easy to track the life-cycle of our service. |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.fibonacciservice" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="8" /> <application android:icon="@drawable/icon" android:label="@string/app_name"> <service android:name=".FibonacciService"> <intent-filter> <action android:name="com.marakana.android.fibonaccicommon.IFibonacciService" /> <!----> </intent-filter> </service> </application> </manifest>
| The name of this action is arbitrary, but it is a common convention to use the fully-qualified name of our AIDL-derived interface. |
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hello">Get Your Fibonacci Here!</string> <string name="app_name">Fibonacci Client</string> <string name="input_hint">Enter N</string> <string name="input_error">Numbers only!</string> <string name="button_text">Get Fib Result</string> <string name="progress_text">Calculating...</string> <string name="fib_error">Failed to get Fibonacci result</string> <string name="type_fib_jr">fibJR</string> <string name="type_fib_ji">fibJI</string> <string name="type_fib_nr">fibNR</string> <string name="type_fib_ni">fibNI</string> </resources>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:text="@string/hello" android:layout_height="wrap_content" android:layout_width="fill_parent" android:textSize="25sp" android:gravity="center"/> <EditText android:layout_height="wrap_content" android:layout_width="match_parent" android:id="@+id/input" android:hint="@string/input_hint" android:inputType="number" android:gravity="right" /> <RadioGroup android:orientation="horizontal" android:layout_width="match_parent" android:id="@+id/type" android:layout_height="wrap_content"> <RadioButton android:layout_height="wrap_content" android:checked="true" android:id="@+id/type_fib_jr" android:text="@string/type_fib_jr" android:layout_width="match_parent" android:layout_weight="1" /> <RadioButton android:layout_height="wrap_content" android:id="@+id/type_fib_ji" android:text="@string/type_fib_ji" android:layout_width="match_parent" android:layout_weight="1" /> <RadioButton android:layout_height="wrap_content" android:id="@+id/type_fib_nr" android:text="@string/type_fib_nr" android:layout_width="match_parent" android:layout_weight="1" /> <RadioButton android:layout_height="wrap_content" android:id="@+id/type_fib_ni" android:text="@string/type_fib_ni" android:layout_width="match_parent" android:layout_weight="1" /> </RadioGroup> <Button android:text="@string/button_text" android:id="@+id/button" android:layout_width="match_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/output" android:layout_width="match_parent" android:layout_height="match_parent" android:textSize="20sp" android:gravity="center|top"/> </LinearLayout>
package com.marakana.android.fibonacciclient; import android.app.Activity; import android.app.ProgressDialog; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.TextView; import android.widget.Toast; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; import com.marakana.android.fibonaccicommon.IFibonacciService; public class FibonacciActivity extends Activity implements OnClickListener, ServiceConnection { private static final String TAG = "FibonacciActivity"; private EditText input; // our input n private Button button; // trigger for fibonacci calcualtion private RadioGroup type; // fibonacci implementation type private TextView output; // destination for fibonacci result private IFibonacciService service; // reference to our service @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.setContentView(R.layout.main); // connect to our UI elements this.input = (EditText) super.findViewById(R.id.input); this.button = (Button) super.findViewById(R.id.button); this.type = (RadioGroup) super.findViewById(R.id.type); this.output = (TextView) super.findViewById(R.id.output); // request button click call-backs via onClick(View) method this.button.setOnClickListener(this); // the button will be enabled once we connect to the service this.button.setEnabled(false); } @Override protected void onResume() { Log.d(TAG, "onResume()'ed"); super.onResume(); // Bind to our FibonacciService service, by looking it up by its name // and passing ourselves as the ServiceConnection object // We'll get the actual IFibonacciService via a callback to // onServiceConnected() below if (!super.bindService(new Intent(IFibonacciService.class.getName()), this, BIND_AUTO_CREATE)) { Log.w(TAG, "Failed to bind to service"); } } @Override protected void onPause() { Log.d(TAG, "onPause()'ed"); super.onPause(); // No need to keep the service bound (and alive) any longer than // necessary super.unbindService(this); } public void onServiceConnected(ComponentName name, IBinder service) { Log.d(TAG, "onServiceConnected()'ed to " + name); // finally we can get to our IFibonacciService this.service = IFibonacciService.Stub.asInterface(service); // enable the button, because the IFibonacciService is initialized this.button.setEnabled(true); } public void onServiceDisconnected(ComponentName name) { Log.d(TAG, "onServiceDisconnected()'ed to " + name); // our IFibonacciService service is no longer connected this.service = null; // disabled the button, since we cannot use IFibonacciService this.button.setEnabled(false); } // handle button clicks public void onClick(View view) { // parse n from input (or report errors) final long n; String s = this.input.getText().toString(); if (TextUtils.isEmpty(s)) { return; } try { n = Long.parseLong(s); } catch (NumberFormatException e) { this.input.setError(super.getText(R.string.input_error)); return; } // build the request object final FibonacciRequest.Type type; switch (FibonacciActivity.this.type.getCheckedRadioButtonId()) { case R.id.type_fib_jr: type = FibonacciRequest.Type.RECURSIVE_JAVA; break; case R.id.type_fib_ji: type = FibonacciRequest.Type.ITERATIVE_JAVA; break; case R.id.type_fib_nr: type = FibonacciRequest.Type.RECURSIVE_NATIVE; break; case R.id.type_fib_ni: type = FibonacciRequest.Type.ITERATIVE_NATIVE; break; default: return; } final FibonacciRequest request = new FibonacciRequest(n, type); // showing the user that the calculation is in progress final ProgressDialog dialog = ProgressDialog.show(this, "", super.getText(R.string.progress_text), true); // since the calculation can take a long time, we do it in a separate // thread to avoid blocking the UI new AsyncTask<Void, Void, String>() { @Override protected String doInBackground(Void... params) { // this method runs in a background thread try { long totalTime = SystemClock.uptimeMillis(); FibonacciResponse response = FibonacciActivity.this.service .fib(request); totalTime = SystemClock.uptimeMillis() - totalTime; // generate the result return String.format( "fibonacci(%d)=%d\nin %d ms\n(+ %d ms)", n, response.getResult(), response.getTimeInMillis(), totalTime - response.getTimeInMillis()); } catch (RemoteException e) { Log.wtf(TAG, "Failed to communicate with the service", e); return null; } } @Override protected void onPostExecute(String result) { // get rid of the dialog dialog.dismiss(); if (result == null) { // handle error Toast.makeText(FibonacciActivity.this, R.string.fib_error, Toast.LENGTH_SHORT).show(); } else { // show the result to the user FibonacciActivity.this.output.setText(result); } } }.execute(); // run our AsyncTask } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.marakana.android.fibonacciclient"> <uses-sdk android:minSdkVersion="8" /> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name="com.marakana.android.fibonacciclient.FibonacciActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
package com.marakana.android.fibonaccicommon; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; oneway interface IFibonacciServiceResponseListener { void onResponse(in FibonacciResponse response); }
package com.marakana.android.fibonaccicommon; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; import com.marakana.android.fibonaccicommon.IFibonacciServiceResponseListener; oneway interface IFibonacciService { void fib(in FibonacciRequest request, in IFibonacciServiceResponseListener listener); }
package com.marakana.android.fibonacciservice; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; import com.marakana.android.fibonaccicommon.IFibonacciService; import com.marakana.android.fibonaccicommon.IFibonacciServiceResponseListener; import com.marakana.android.fibonaccinative.FibLib; public class IFibonacciServiceImpl extends IFibonacciService.Stub { private static final String TAG = "IFibonacciServiceImpl"; @Override public void fib(FibonacciRequest request, IFibonacciServiceResponseListener listener) throws RemoteException { long n = request.getN(); Log.d(TAG, "fib(" + n + ")"); long timeInMillis = SystemClock.uptimeMillis(); long result; switch (request.getType()) { case ITERATIVE_JAVA: result = FibLib.fibJI(n); break; case RECURSIVE_JAVA: result = FibLib.fibJR(n); break; case ITERATIVE_NATIVE: result = FibLib.fibNI(n); break; case RECURSIVE_NATIVE: result = FibLib.fibNR(n); break; default: result = 0; } timeInMillis = SystemClock.uptimeMillis() - timeInMillis; Log.d(TAG, String.format("Got fib(%d) = %d in %d ms", n, result, timeInMillis)); listener.onResponse(new FibonacciResponse(result, timeInMillis)); } }
|
|
The service will not block waiting for the listener to return, because the listener itself is also oneway. |
package com.marakana.android.fibonacciclient; import android.app.Activity; import android.app.Dialog; import android.app.ProgressDialog; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.RemoteException; import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.TextView; import com.marakana.android.fibonaccicommon.FibonacciRequest; import com.marakana.android.fibonaccicommon.FibonacciResponse; import com.marakana.android.fibonaccicommon.IFibonacciService; import com.marakana.android.fibonaccicommon.IFibonacciServiceResponseListener; public class FibonacciActivity extends Activity implements OnClickListener, ServiceConnection { private static final String TAG = "FibonacciActivity"; // the id of a message to our response handler private static final int RESPONSE_MESSAGE_ID = 1; // the id of a progress dialog that we'll be creating private static final int PROGRESS_DIALOG_ID = 1; private EditText input; // our input n private Button button; // trigger for fibonacci calcualtion private RadioGroup type; // fibonacci implementation type private TextView output; // destination for fibonacci result private IFibonacciService service; // reference to our service // the responsibility of the responseHandler is to take messages // from the responseListener (defined below) and display their content // in the UI thread private final Handler responseHandler = new Handler() { @Override public void handleMessage(Message message) { switch (message.what) { case RESPONSE_MESSAGE_ID: Log.d(TAG, "Handling response"); FibonacciActivity.this.output.setText((String) message.obj); FibonacciActivity.this.removeDialog(PROGRESS_DIALOG_ID); break; } } }; // the responsibility of the responseListener is to receive call-backs // from the service when our FibonacciResponse is available private final IFibonacciServiceResponseListener responseListener = new IFibonacciServiceResponseListener.Stub() { // this method is executed on one of the pooled binder threads @Override public void onResponse(FibonacciResponse response) throws RemoteException { String result = String.format("%d in %d ms", response.getResult(), response.getTimeInMillis()); Log.d(TAG, "Got response: " + result); // since we cannot update the UI from a non-UI thread, // we'll send the result to the responseHandler (defined above) Message message = FibonacciActivity.this.responseHandler .obtainMessage(RESPONSE_MESSAGE_ID, result); FibonacciActivity.this.responseHandler.sendMessage(message); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.setContentView(R.layout.main); // connect to our UI elements this.input = (EditText) super.findViewById(R.id.input); this.button = (Button) super.findViewById(R.id.button); this.type = (RadioGroup) super.findViewById(R.id.type); this.output = (TextView) super.findViewById(R.id.output); // request button click call-backs via onClick(View) method this.button.setOnClickListener(this); // the button will be enabled once we connect to the service this.button.setEnabled(false); } @Override protected void onStart() { Log.d(TAG, "onStart()'ed"); super.onStart(); // Bind to our FibonacciService service, by looking it up by its name // and passing ourselves as the ServiceConnection object // We'll get the actual IFibonacciService via a callback to // onServiceConnected() below if (!super.bindService(new Intent(IFibonacciService.class.getName()), this, BIND_AUTO_CREATE)) { Log.w(TAG, "Failed to bind to service"); } } @Override protected void onStop() { Log.d(TAG, "onStop()'ed"); super.onStop(); // No need to keep the service bound (and alive) any longer than // necessary super.unbindService(this); } public void onServiceConnected(ComponentName name, IBinder service) { Log.d(TAG, "onServiceConnected()'ed to " + name); // finally we can get to our IFibonacciService this.service = IFibonacciService.Stub.asInterface(service); // enable the button, because the IFibonacciService is initialized this.button.setEnabled(true); } public void onServiceDisconnected(ComponentName name) { Log.d(TAG, "onServiceDisconnected()'ed to " + name); // our IFibonacciService service is no longer connected this.service = null; // disabled the button, since we cannot use IFibonacciService this.button.setEnabled(false); } @Override protected Dialog onCreateDialog(int id) { switch (id) { case PROGRESS_DIALOG_ID: // this dialog will be opened in onClick(...) and // dismissed/removed by responseHandler.handleMessage(...) ProgressDialog dialog = new ProgressDialog(this); dialog.setMessage(super.getText(R.string.progress_text)); dialog.setIndeterminate(true); return dialog; default: return super.onCreateDialog(id); } } // handle button clicks public void onClick(View view) { // parse n from input (or report errors) final long n; String s = this.input.getText().toString(); if (TextUtils.isEmpty(s)) { return; } try { n = Long.parseLong(s); } catch (NumberFormatException e) { this.input.setError(super.getText(R.string.input_error)); return; } // build the request object final FibonacciRequest.Type type; switch (FibonacciActivity.this.type.getCheckedRadioButtonId()) { case R.id.type_fib_jr: type = FibonacciRequest.Type.RECURSIVE_JAVA; break; case R.id.type_fib_ji: type = FibonacciRequest.Type.ITERATIVE_JAVA; break; case R.id.type_fib_nr: type = FibonacciRequest.Type.RECURSIVE_NATIVE; break; case R.id.type_fib_ni: type = FibonacciRequest.Type.ITERATIVE_NATIVE; break; default: return; } final FibonacciRequest request = new FibonacciRequest(n, type); try { Log.d(TAG, "Submitting request..."); long time = SystemClock.uptimeMillis(); // submit the request; the response will come to responseListener this.service.fib(request, this.responseListener); time = SystemClock.uptimeMillis() - time; Log.d(TAG, "Submited request in " + time + " ms"); // this dialog will be dismissed/removed by responseHandler super.showDialog(PROGRESS_DIALOG_ID); } catch (RemoteException e) { Log.wtf(TAG, "Failed to communicate with the service", e); } } }
Create an AIDL-described ILogService that provides the following functionality:
package com.marakana.android.logservice; public interface ILogService { public void log(LogMessage logMessage); }
where LogMessage is defined as follows:
package com.marakana.android.logservice; public class LogMessage … { … public LogMessage(int priority, String tag, String msg) { … } … }
Create a simple Android client that allows the user to submit a LogMessage request to the remote ILogService running in a separate process.
You can borrow code (UI) from the basic LogNative application, which you can get:
|
|
Your implementation could simply use android.util.Log.println(int priority, String tag, String msg) to do the logging. |
The solution is provided:
Why do we need IPC on Android?
What is Binder’s unit of data called?
What is AIDL and when do we use it?
Is the use of AIDL required when consuming or exposing bound services?
Name at least five supported method parameter/return data types that we use in AIDL.
Name at least three types that are not supported.
What is android.os.Bundle?
What is android.os.Parcelable and how do we implement it?
What do we have to do with android.os.Parcelable classes before we can use them in AIDL interfaces?
What is android.os.IBinder?
What is special about file descriptors?
What is the purpose of the directional flag?
What is the purpose of the aidl tool?
What is the purpose of the Stub and the Proxy?
What is the Android library project and when do we use them?
What is the purpose of android.app.Service in the context of bound services?
How do we expose a bound service to other applications?
What is the thread context of an binder service request?
How and when do we bind to non-system services?
What is the life-cycle dependency between bound services and their clients?
What is the purpose of the oneway keyword and when do we use it?
What do we have to worry about when handling call-backs from remote services?
$ keytool -genkey -v -keystore marakana.keystore -alias android -keyalg RSA -keysize 2048 -validity 10000 \ -dname "CN=Android Application Signer, OU=Android, O=Marakana Inc., L=San Francisco, ST=California, C=US"
$ jarsigner -keystore marakana.keystore MyApp.apk android $ jarsigner -verify MyApp.apk jar verified.
$ jar -xvf MyApp.apk META-INF/CERT.RSA
$ keytool -printcert -file META-INF/CERT.RSA
Owner: CN=Android Application Signer, OU=Android, O=Marakana Inc., L=San Francisco, ST=California, C=US
Issuer: CN=Android Application Signer, OU=Android, O=Marakana Inc., L=San Francisco, ST=California, C=US
Serial number: 4f4e6461
Valid from: Wed Feb 29 11:46:09 CST 2012 until: Sun Feb 22 11:46:09 CST 2037
Certificate fingerprints:
MD5: 57:B8:23:06:09:CD:2A:C9:9C:02:6C:B4:5D:1C:34:BD
SHA1: 2E:49:E4:20:81:9E:A3:E9:91:5D:99:57:2A:A6:88:0E:23:14:63:AA
Signature algorithm name: SHA1withRSA
Version: 3
$ rm META-INF/CERT.RSA && rmdir META-INF
$ zipalign -v 4 UnalignedApp.apk AlignedApp.apk
$ rm build/target/product/security/platform.p*
$ SIGNER="/C=US/ST=California/L=San Francisco/O=Marakana Inc./OU=Android/CN=Android Platform Signer/emailAddress=android@marakana.com"
$ echo | development/tools/make_key build/target/product/security/platform "$SIGNER"
Repeat for media, shared, and testkey
$ java -jar /path/to/aosp/out/host/linux-x86/framework/signapk.jar \
/path/to/aosp/build/target/product/security/platform.x509.pem \
/path/to/aosp/build/target/product/security/platform.pk8 \
MyApp.apk MySignedApp.apk
$ adb -e shell cat /data/system/packages.list |grep com.android.browser com.android.browser 10001 0 /data/data/com.android.browser
$ adb -e shell ps |grep com.android.browser app_1 682 37 192592 53144 ffffffff 40011384 S com.android.browser
adb -e shell ls -l /data/data/com.android.browser drwxrwx--x app_1 app_1 2012-02-06 12:47 app_appcache drwxrwx--x app_1 app_1 2012-02-06 12:47 app_databases drwxrwx--x app_1 app_1 2012-02-06 12:47 app_geolocation drwxrwx--x app_1 app_1 2012-02-06 12:47 app_icons drwxrwx--x app_1 app_1 2012-02-06 12:47 cache drwxrwx--x app_1 app_1 2012-02-06 12:48 databases drwxr-xr-x system system 2012-01-17 15:42 lib drwxrwx--x app_1 app_1 2012-02-06 12:47 shared_prefs
<manifest package="com.marakana.android.myapp" android:sharedUserId="marakana.uid.myapp" android:sharedUserLabel="@string/myapp_uid" … > <application android:process="myapp" … > … </application> </manifest>
|
|
From the security standpoint, packages with the same sharedUserId are treated as being parts of the same application, with the same UID and file permissions. |
$ adb shell mount rootfs / rootfs ro 0 0 tmpfs /dev tmpfs rw,nosuid,mode=755 0 0 devpts /dev/pts devpts rw,mode=600 0 0 proc /proc proc rw 0 0 sysfs /sys sysfs rw 0 0 none /acct cgroup rw,cpuacct 0 0 tmpfs /mnt/asec tmpfs rw,mode=755,gid=1000 0 0 tmpfs /mnt/obb tmpfs rw,mode=755,gid=1000 0 0 none /dev/cpuctl cgroup rw,cpu 0 0 /dev/block/mtdblock0 /system yaffs2 ro 0 0 /dev/block/mtdblock1 /data yaffs2 rw,nosuid,nodev 0 0 /dev/block/mtdblock2 /cache yaffs2 rw,nosuid,nodev 0 0 /dev/block/vold/179:0 /mnt/sdcard vfat rw,dirsync,nosuid,nodev,noexec,uid=1000,gid=1015,fmask=0702,dmask=0702,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0 /dev/block/vold/179:0 /mnt/secure/asec vfat rw,dirsync,nosuid,nodev,noexec,uid=1000,gid=1015,fmask=0702,dmask=0702,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0 tmpfs /mnt/sdcard/.android_secure tmpfs ro,size=0k,mode=000 0 0
$ adb shell pm path com.android.browser package:/system/app/Browser.apk
$ adb shell run-as com.example.helloworld am start -a android.intent.action.CALL -d tel:4155551234
Starting: Intent { act=android.intent.action.CALL dat=tel:xxxxxxxxxx }
java.lang.SecurityException: Permission Denial: starting Intent { act=android.intent.action.CALL dat=tel:xxxxxxxxxx flg=0x10000000 cmp=com.android.phone/.OutgoingCallBroadcaster } from null (pid=1533, uid=10045) requires android.permission.CALL_PHONE
at android.os.Parcel.readException(Parcel.java:1327)
at android.os.Parcel.readException(Parcel.java:1281)
at android.app.ActivityManagerProxy.startActivity(ActivityManagerNative.java:1631)
at com.android.commands.am.Am.runStart(Am.java:433)
at com.android.commands.am.Am.run(Am.java:107)
at com.android.commands.am.Am.main(Am.java:80)
at com.android.internal.os.RuntimeInit.finishInit(Native Method)
at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:238)
at dalvik.system.NativeStart.main(Native Method)
$ adb shell run-as com.example.helloworld ls /data/misc/keystore opendir failed, Permission denied
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.trackapp"> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.SEND_SMS" /> … </manifest>
adb shell pm list permissions All Permissions: permission:android.permission.CLEAR_APP_USER_DATA permission:android.permission.SHUTDOWN permission:android.permission.BIND_INPUT_METHOD permission:android.permission.ACCESS_DRM permission:android.permission.DOWNLOAD_CACHE_NON_PURGEABLE permission:android.permission.INTERNAL_SYSTEM_WINDOW permission:android.permission.SEND_DOWNLOAD_COMPLETED_INTENTS permission:android.permission.MOVE_PACKAGE permission:android.permission.ACCESS_CHECKIN_PROPERTIES permission:android.permission.CRYPT_KEEPER permission:android.permission.READ_INPUT_STATE permission:android.permission.DEVICE_POWER permission:android.permission.DELETE_PACKAGES permission:android.permission.ACCESS_CACHE_FILESYSTEM permission:android.permission.REBOOT permission:android.permission.STATUS_BAR permission:android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED permission:android.permission.ACCESS_ALL_DOWNLOADS permission:android.permission.STOP_APP_SWITCHES permission:android.permission.BIND_VPN_SERVICE permission:android.permission.CONTROL_LOCATION_UPDATES permission:android.permission.ACCESS_DOWNLOAD_MANAGER permission:android.permission.MANAGE_APP_TOKENS permission:android.permission.BIND_PACKAGE_VERIFIER permission:android.permission.DELETE_CACHE_FILES permission:android.permission.BATTERY_STATS permission:android.permission.COPY_PROTECTED_DATA permission:com.android.email.permission.ACCESS_PROVIDER permission:android.permission.INSTALL_DRM permission:android.permission.MASTER_CLEAR permission:android.permission.SET_ACTIVITY_WATCHER permission:android.permission.BRICK permission:android.permission.MODIFY_NETWORK_ACCOUNTING permission:android.permission.READ_NETWORK_USAGE_HISTORY permission:android.permission.BACKUP permission:android.permission.SET_TIME permission:android.permission.STATUS_BAR_SERVICE permission:android.permission.INSTALL_PACKAGES permission:android.permission.PERFORM_CDMA_PROVISIONING permission:android.permission.INJECT_EVENTS permission:android.permission.SET_POINTER_SPEED permission:com.android.browser.permission.PRELOAD permission:android.permission.WRITE_SECURE_SETTINGS permission:android.permission.INSTALL_LOCATION_PROVIDER permission:android.permission.CONFIRM_FULL_BACKUP permission:android.permission.PACKAGE_USAGE_STATS permission:android.permission.ACCESS_SURFACE_FLINGER permission:android.permission.CALL_PRIVILEGED permission:android.permission.PACKAGE_VERIFICATION_AGENT permission:android.permission.CHANGE_COMPONENT_ENABLED_STATE permission:android.intent.category.MASTER_CLEAR.permission.C2D_MESSAGE permission:android.permission.WRITE_GSERVICES permission:android.permission.MANAGE_NETWORK_POLICY permission:android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK permission:android.permission.BIND_TEXT_SERVICE permission:android.permission.READ_FRAME_BUFFER permission:android.permission.FORCE_BACK permission:android.permission.UPDATE_DEVICE_STATS permission:android.permission.BIND_WALLPAPER permission:android.permission.BIND_REMOTEVIEWS permission:android.permission.SET_ORIENTATION permission:android.permission.FACTORY_TEST permission:android.permission.BIND_DEVICE_ADMIN
|
|
Add -f for the full description of permissions - i.e. adb shell pm list permissions -f |
Using the following permissions will significantly lower the likelihood for an Android app/game to be featured in Google Play (from Google I/O 2012):
android.permission.SEND_SMS and android.permission.RECEIVE_SMS
android.permission.SYSTEM_ALERT_WINDOW
com.android.browser.permission.READ_HISTORY_BOOKMARKS and com.android.browser.permission.WRITE_HISTORY_BOOKMARKS
android.permission.READ_CONTACTS, android.permission.WRITE_CONTACTS, android.permission.READ_CALENDAR, android.permission.WRITE_CALENDAR
android.permission.CALL_PHONE
android.permission.READ_LOGS
android.permission.ACCESS_FINE_LOCATION
android.permission.GET_TASKS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.CHANGE_WIFI_STATE
... public class MyActivity extends Activity { private static final int CAPTURE_IMAGE_REQ = 1; ... public void onClick(View view) { Intent intent = new Intent( android.provider.MediaStore.ACTION_IMAGE_CAPTURE, CAPTURE_IMAGE_REQ); intent.putExtra( android.provider.MediaStore.EXTRA_OUTPUT, "/sdcard/myphoto.jpeg"); startActivityForResult(intent); } protected void onActivityResult (int requestCode, int resultCode, Intent data) { switch (requestCode) { case CAPTURE_IMAGE_REQ: if (resultCode == RESULT_OK) { // we have the image! } break; } } }
... public class MyActivity extends Activity { ... public void onClick(View view) { Uri smsNumber = Uri.parse("sms:14155551234"); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(smsNumber); intent.putExtra(Intent.EXTRA_TEXT, "Hello"); } }
... public class MyActivity extends Activity { private static final int GET_CONTACT_REQ = 1; ... public void onClick(View view) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType(Phone.CONTENT_ITEM_TYPE); startActivityForResult(intent, GET_CONTACT_REQ); } public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == GET_CONTACT_REQ && resultCode == RESULT_OK && data != null) { Uri uri = data.getData(); if (uri != null) { try { // best to do on another thread Cursor c = getContentResolver().query(uri, new String[] {Contacts.DISPLAY_NAME, Phone.NUMBER}, null, null, null); if (c.moveToFirst()) { String name = c.getString(0); String phone = c.getString(1); // we have the contact info! } } catch (RuntimeException e) { // handle } } } } }
String deviceId = UUID.randomUUID().toString(); // store deviceId in the application preferences
|
|
For more choices on how to get a unique device ID, see http://android-developers.blogspot.com/2011/03/identifying-app-installations.html |
There are a number of trigger points for security/permission checks:
<permissions> … <permission name="android.permission.INTERNET" > <group gid="inet" /> </permission> <permission name="android.permission.CAMERA" > <group gid="camera" /> </permission> <permission name="android.permission.READ_LOGS" > <group gid="log" /> </permission> <permission name="android.permission.WRITE_EXTERNAL_STORAGE" > <group gid="sdcard_rw" /> </permission> … </permissions>
|
|
Run adb shell cat /system/etc/permissions/platform.xml to see all the mappings |
… static struct { unsigned uid; const char *name; } allowed[] = { #ifdef LVMX { AID_MEDIA, "com.lifevibes.mx.ipc" }, #endif { AID_MEDIA, "media.audio_flinger" }, { AID_MEDIA, "media.player" }, { AID_MEDIA, "media.camera" }, { AID_MEDIA, "media.audio_policy" }, { AID_DRM, "drm.drmManager" }, { AID_NFC, "nfc" }, { AID_RADIO, "radio.phone" }, { AID_RADIO, "radio.sms" }, { AID_RADIO, "radio.phonesubinfo" }, { AID_RADIO, "radio.simphonebook" }, /* TODO: remove after phone services are updated: */ { AID_RADIO, "phone" }, { AID_RADIO, "sip" }, { AID_RADIO, "isms" }, { AID_RADIO, "iphonesubinfo" }, { AID_RADIO, "simphonebook" }, }; … int svc_can_register(unsigned uid, uint16_t *name) { unsigned n; if ((uid == 0) || (uid == AID_SYSTEM)) return 1; for (n = 0; n < sizeof(allowed) / sizeof(allowed[0]); n++) if ((uid == allowed[n].uid) && str16eq(name, allowed[n].name)) return 1; return 0; } …
<manifest …> … <application …> … <activity android:name=".GetPasswordActivity" android:permission="com.marakana.android.permission.GET_PASSWORD_FROM_USER" … > … </activity> <service android:name=".UserAuthenticatorService" android:permission="com.marakana.android.permission.AUTHENTICATE_USER" … > … </service> <provider android:name=".EnterpriseDataProvider" android:readPermission="com.marakana.android.permission.READ_ENTERPRISE_DATA" android:writePermission="com.marakana.android.permission.WRITE_ENTERPRISE_DATA" … > … </provider> <receiver android:name=".UserAuthStatusReceiver" android:permission="com.marakana.android.permission.SEND_USER_AUTH_STATUS"> … </receiver> </application> </manifest>
|
|
If the permission is defined in the same application (which is the most usual case), then we can statically access its name via the auto-generated Manifest class. For example: Manifest.permission.MY_PERMISSION |
package com.android.server; … public class VibratorService extends IVibratorService.Stub { … public void vibrate(long milliseconds, IBinder token) { if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.VIBRATE) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires VIBRATE permission"); } … } … }
package com.android.server; … public class LocationManagerService extends ILocationManager.Stub implements Runnable { … private static final String ACCESS_FINE_LOCATION = android.Manifest.permission.ACCESS_FINE_LOCATION; private static final String ACCESS_COARSE_LOCATION = android.Manifest.permission.ACCESS_COARSE_LOCATION; … private void checkPermissionsSafe(String provider) { if ((LocationManager.GPS_PROVIDER.equals(provider) || LocationManager.PASSIVE_PROVIDER.equals(provider)) && (mContext.checkCallingOrSelfPermission(ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED)) { throw new SecurityException("Provider " + provider + " requires ACCESS_FINE_LOCATION permission"); } if (LocationManager.NETWORK_PROVIDER.equals(provider) && (mContext.checkCallingOrSelfPermission(ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) && (mContext.checkCallingOrSelfPermission(ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED)) { throw new SecurityException("Provider " + provider + " requires ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION permission"); } } … private Location _getLastKnownLocationLocked(String provider) { checkPermissionsSafe(provider); … } … public Location getLastKnownLocation(String provider) { … _getLastKnownLocationLocked(provider); … } }
Before we can enforce our own permissions, we have to declare them using one or more <permission> in
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.myapp" > <permission android:name="com.example.app.DO_X" android:label="@string/do_x_label" android:description="@string/do_x_desc" android:permissionGroup="android.permission-group.PERSONAL_INFO" android:protectionLevel="dangerous" /> … </manifest>
$ adb shell pm list permission-groups permission group:android.permission-group.DEVELOPMENT_TOOLS permission group:android.permission-group.PERSONAL_INFO permission group:android.permission-group.COST_MONEY permission group:android.permission-group.LOCATION permission group:android.permission-group.MESSAGES permission group:android.permission-group.NETWORK permission group:android.permission-group.ACCOUNTS permission group:android.permission-group.STORAGE permission group:android.permission-group.PHONE_CALLS permission group:android.permission-group.HARDWARE_CONTROLS permission group:android.permission-group.SYSTEM_TOOLS
|
|
We can list permissions currently defined an an Android device/emulator: $ adb shell pm list permissions -s |
<manifest …> … <permission-tree android:name="com.marakana.android.foo" /> … </manifest>
… public class FooActivity extends Activity … { public void onCreate() { super.onCreate(); … PermissionInfo permission = new PermissionInfo(); permission.name = "com.marakana.android.foo.DO_X_WITH_FOO"; permission.protectionLevel = PermissionInfo.PROTECTION_SIGNATURE; super.getPackageManager().addPermission(permission); … } }
Here, we want to restrict access to the com.marakana.android.fibonacciservice.FibonacciService to applications (i.e. clients) that hold USE_FIBONACCI_SERVICE custom permission
We start by by creating a custom permission group (making sure that we name-space it):
<?xml version="1.0" encoding="utf-8"?> <resources> … <string name="fibonacci_permissions_group_label">Fibonacci Permissions</string> … </resources>
<?xml version="1.0" encoding="utf-8"?> <manifest …> … <permission-group android:name="com.marakana.android.fibonacciservice.FIBONACCI_PERMISSIONS" android:label="@string/fibonacci_permissions_group_label" /> … </manifest>
|
|
This permission group is optional - as we could instead use one of the already provided groups |
Next, we create a custom permission (again, making sure that we name-space it), while taking advantage of our newly-created permission group:
<?xml version="1.0" encoding="utf-8"?> <resources> … <string name="use_fibonacci_service_permission_label">use fibonacci service</string> <string name="use_fibonacci_service_permission_description"> applications with this permissions get fibonacci results for free </string> … </resources>
<?xml version="1.0" encoding="utf-8"?> <manifest …> … <permission-group …/> <permission android:name="com.marakana.android.fibonacciservice.USE_FIBONACCI_SERVICE" android:description="@string/use_fibonacci_service_permission_description" android:label="@string/use_fibonacci_service_permission_label" android:permissionGroup="com.marakana.android.fibonacciservice.FIBONACCI_PERMISSIONS" android:protectionLevel="dangerous" /> … </manifest>
Now we can statically require the permission on our FibonacciService service:
<?xml version="1.0" encoding="utf-8"?> <manifest …> … <permission-group …/> <permission …/> <application …> <service android:name=".FibonacciService" android:permission="com.marakana.android.fibonacciservice.USE_FIBONACCI_SERVICE" > … </service> </application> … </manifest>
If we now re-run the FibonacciService and re-run the FibonacciClient, we will notice that the client will fail to launch and adb logcat will show something like:
…
W/ActivityManager( 85): Permission Denial: Accessing service ComponentInfo{com.marakana.android.fibonacciservice/com.marakana.android.fibonacciservice.FibonacciService} from pid=540, uid=10043 requires com.marakana.android.fibonacciservice.USE_FIBONACCI_SERVICE
D/AndroidRuntime( 540): Shutting down VM
W/dalvikvm( 540): threadid=1: thread exiting with uncaught exception (group=0x409c01f8)
E/AndroidRuntime( 540): FATAL EXCEPTION: main
E/AndroidRuntime( 540): java.lang.RuntimeException: Unable to resume activity {com.marakana.android.fibonacciclient/com.marakana.android.fibonacciclient.FibonacciActivity}: java.lang.SecurityException: Not allowed to bind to service Intent { act=com.marakana.android.fibonaccicommon.IFibonacciService }
E/AndroidRuntime( 540): at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2444)
…
E/AndroidRuntime( 540): at dalvik.system.NativeStart.main(Native Method)
E/AndroidRuntime( 540): Caused by: java.lang.SecurityException: Not allowed to bind to service Intent { act=com.marakana.android.fibonaccicommon.IFibonacciService }
E/AndroidRuntime( 540): at android.app.ContextImpl.bindService(ContextImpl.java:1135)
E/AndroidRuntime( 540): at android.content.ContextWrapper.bindService(ContextWrapper.java:370)
E/AndroidRuntime( 540): at com.marakana.android.fibonacciclient.FibonacciActivity.onResume(FibonacciActivity.java:65)
…
W/ActivityManager( 85): Force finishing activity com.marakana.android.fibonacciclient/.FibonacciActivity
…
Finally, we can give FibonacciClient a fighting chance by allowing it to use the USE_FIBONACCI_SERVICE permission:
<?xml version="1.0" encoding="utf-8"?> <manifest …> … <uses-permission android:name="com.marakana.android.fibonacciservice.USE_FIBONACCI_SERVICE"/> … </manifest>
We can now observe that our client is again able to use the service
In the Emulator, if we go to Home → Menu → Manage apps → Fibonacci Client → PERMISSIONS, we should see the Fibonacci Permissions group and under it, use fibonacci service permission
Here, we want to restrict access to the com.marakana.android.fibonacciservice.IFibonacciServiceImpl's recursive operations (fibJR(long n) and fibNR(long n)) for n > 10 to applications (i.e. clients) that hold USE_SLOW_FIBONACCI_SERVICE custom permission
Like before, we start off by creating a custom permission:
<?xml version="1.0" encoding="utf-8"?> <resources> … <string name="use_slow_fibonacci_service_permission_label"> use slow fibonacci service operations </string> <string name="use_slow_fibonacci_service_permission_description"> applications with this permissions can melt the CPU and drain the battery by using slow fibonacci operations </string> … </resources>
<?xml version="1.0" encoding="utf-8"?> <manifest …> … <permission-group …/> <permission …/> <permission android:name="com.marakana.android.fibonacciservice.USE_SLOW_FIBONACCI_SERVICE" android:description="@string/use_slow_fibonacci_service_permission_description" android:label="@string/use_slow_fibonacci_service_permission_label" android:permissionGroup="com.marakana.android.fibonacciservice.FIBONACCI_PERMISSIONS" android:protectionLevel="dangerous" /> … </manifest>
Next, we update our IFibonacciServiceImpl to enforce this permission dynamically - via a android.content.Context that get expect to get through the constructor:
package com.marakana.android.fibonacciservice; import android.content.Context; … public class IFibonacciServiceImpl extends IFibonacciService.Stub { … private final Context context; public IFibonacciServiceImpl(Context context) { this.context = context; } private long checkN(long n) { if (n > 10) { this.context.enforceCallingOrSelfPermission( Manifest.permission.USE_SLOW_FIBONACCI_SERVICE, "Go away!"); } return n; } … public long fibJR(long n) { … return FibLib.fibJR(this.checkN(n)); } … public long fibNR(long n) { … return FibLib.fibNR(this.checkN(n)); } … }
We have to update FibonacciService to invoke the new IFibonacciServiceImpl's constructor:
… public class FibonacciService extends Service { … @Override public void onCreate() { … this.service = new IFibonacciServiceImpl(super.getApplicationContext()); … } … }
If we now re-run the FibonacciService and re-run the FibonacciClient for a recursive operation with n > 10, we will notice that the client will fail and adb logcat will show something like:
… D/IFibonacciServiceImpl( 617): fib(15, RECURSIVE_NATIVE) D/IFibonacciServiceImpl( 617): fibNR(15) W/dalvikvm( 604): threadid=11: thread exiting with uncaught exception (group=0x409c01f8) E/AndroidRuntime( 604): FATAL EXCEPTION: AsyncTask #1 E/AndroidRuntime( 604): java.lang.RuntimeException: An error occured while executing doInBackground() … E/AndroidRuntime( 604): at java.lang.Thread.run(Thread.java:856) E/AndroidRuntime( 604): Caused by: java.lang.SecurityException: Go away!: Neither user 10043 nor current process has com.marakana.android.fibonacciservice.USE_SLOW_FIBONACCI_SERVICE. …
Finally, we can allow FibonacciClient to melt our CPU and drain our battery by allowing it to use the USE_SLOW_FIBONACCI_SERVICE permission:
<?xml version="1.0" encoding="utf-8"?> <manifest …> … <uses-permission android:name="com.marakana.android.fibonacciservice.USE_SLOW_FIBONACCI_SERVICE"/> … </manifest>
We can now observe that our client is again able to use recursive fibonacci operations even for n > 10
In the Emulator, if we go to Home → Menu → Manage apps → Fibonacci Client → PERMISSIONS → Fibonacci Permissions, we should see both use fibonacci service and use slow fibonacci service operations permissions
package com.marakana.android.logcommon; import com.marakana.android.logcommon.LogMessage; interface ILogService { void log(in LogMessage logMessage); }
package com.marakana.android.logcommon; import android.os.Parcel; import android.os.Parcelable; public class LogMessage implements Parcelable { private final int priority; private final String tag; private final String msg; public LogMessage(int priority, String tag, String msg) { this.priority = priority; this.tag = tag; this.msg = msg; } public int getPriority() { return priority; } public String getTag() { return tag; } public String getMsg() { return msg; } … }
Get LogCommon, LogService, and LogClient Eclipse projects
Unzip or git clone into your workspace directory
Import LogCommon, LogClient, and LogService (as existing) projects into Eclipse
|
|
If you get errors on import, try to clean all projects (menubar → Project → Clean… → Clean all projects → OK), and/or close and reopen all projects, and/or restart Eclipse |
Restrict access to the com.marakana.android.logservice.LogService to applications that hold USE_LOG_SERVICE custom permission
Create a custom permission group (make sure to name-space it)
Create a custom permission (make sure to name-space it)
Then require the permission on the service
Test that a client without the required permission cannot bind to the service
Look for an exception stack trace in adb logcat when you launch the client
Have the client use the required permission
Test again - the client should now be able to bind to the service as before
Restrict access to "long" log messages on com.marakana.android.logservice.ILogService to applications that hold USE_LONG_LOG_SERVICE permission (e.g. where "long" == logMessage.getTag().length() > 10 || logMessage.getMsg().length() > 80)
Create another custom permission (make sure to name-space it)
Dynamically enforce your permission
|
|
For you to do this, you’ll need access to a android.content.Context object inside the provided com.marakana.android.logservice.ILogServiceImpl. Conveniently enough, com.marakana.android.logservice.LogService extends android.app.Service, which in turn implements android.content.Context and has access to application context via getApplicationContext() method. |
Test that a client without the required permission cannot log "long" messages
Have the client use the required permission
Test again - the client should now be able to use the service as before
<manifest …> … <application …> … <activity android:name=".GetPasswordActivity" android:exported="false" … > <intent-filter> … </intent-filter> </activity> </application> </manifest>
KeyStore keyStore = ...; KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509"); kmf.init(keyStore); SSLContext context = SSLContext.getInstance("TLS"); context.init(kmf.getKeyManagers(), null, null); URL url = new URL("https://www.example.com/"); HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); urlConnection.setSSLSocketFactory(context.getSocketFactory()); InputStream in = urlConnection.getInputStream(); ...
int timeout = 10000; String host = "www.fortify.net"; int port = 443; SSLSessionCache cache = new SSLSessionCache((Context) this); Socket socket = SSLCertificateSocketFactory.getDefault(timeout, cache).createSocket(host, port); OutputStream out = socket.getOutputStream(); InputStream in = socket.getInputStream(); String request = "GET /sslcheck.html HTTP/1.1\r\nHost: 68.178.217.222\r\n\r\n"; out.write(request.getBytes()); byte[] response = new byte[1024]; int nRead = in.read(response);
package com.marakana.android.securenote; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.spec.InvalidParameterSpecException; import java.util.Arrays; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public class CryptUtil { public static final int IV_LENGTH = 16; private static final String ENCRYPTION_ALGORITHM = "AES/CBC/PKCS5Padding"; private static final String KEY_ALGORITHM = "AES"; private static final int KEY_SIZE = 256; public static Key getKey(byte[] secret) throws NoSuchAlgorithmException { return getKey(secret, false); } public static Key getKey(byte[] secret, boolean wipeSecret) throws NoSuchAlgorithmException { // generate an encryption/decryption key from random data seeded with // our secret (i.e. password) SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG"); secureRandom.setSeed(secret); KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM); keyGenerator.init(KEY_SIZE, secureRandom); Key key = new SecretKeySpec(keyGenerator.generateKey().getEncoded(), KEY_ALGORITHM); if (wipeSecret) { Arrays.fill(secret, (byte)0); } return key; } public static Cipher getEncryptCipher(Key key) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException { Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, key); return cipher; } public static Cipher getDecryptCipher(Key key, byte[] iv) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); return cipher; } public static byte[] getIv(Cipher cipher) throws InvalidParameterSpecException { return cipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV(); } public static byte[] getIv(InputStream in) throws IOException { byte[] iv = new byte[IV_LENGTH]; for (int i = 0; i < iv.length;) { int nRead = in.read(iv, i, iv.length - i); if (nRead == -1) { throw new EOFException("Unexpected EOF"); } else { i += nRead; } } return iv; } }
byte[] secret = // some secret byte[] plainText = // some plain text Key key = CryptUtil.getKey(secret, true); Cipher cipher = CryptUtil.getEncryptCipher(key); byte[] iv = CryptUtil.getIv(cipher); byte[] encrypted = cipher.doFinal(plainText); // store iv and encrypted
byte[] secret = // some secret byte[] encrypted = // read encrypted buffer from somewhere byte[] iv = // read iv from somewhere Key key = CryptUtil.getKey(secret, true); Cipher cipher = CryptUtil.getDecryptCipher(key, iv); byte[] plainText = cipher.doFinal(encrypted);
byte[] secret = // some secret byte[] plainText = // some plain text Key key = CryptUtil.getKey(secret, true); OutputStream out = new FileOutputStream("some-file"); // or = socket.getOutputStream() try { Cipher cipher = CryptUtil.getEncryptCipher(key); byte[] iv = CryptUtil.getIv(cipher); out.write(iv); out = new CipherOutputStream(out, cipher); out.write(plainText); out.flush(); } finally { out.close(); }
InputStream in = new FileInputStream("some-file"); // or = socket.getInputStream(); try { byte[] iv = CryptUtil.getIv(in); Cipher cipher = CryptUtil.getDecryptCipher(key, iv); in = new CipherInputStream(in, cipher); in.read(plainText); // do properly } finally { in.close(); }
|
|
In May 2011, Google was caught with their pants down. They passed authentication tokens from their Android client applications to their backend services, including contacts, calendars, and photos (picassa) over a plain-text (i.e. unencrypted) channel. This enabled potential attackers to get access and modify private content of users whose auth tokens were captured. See http://money.cnn.com/2011/05/18/technology/android_security/?section=money_latest |
$ adb shell mount |grep /data /dev/block/mmcblk0p8 /data ext4 rw,nosuid,nodev,noatime,barrier=1,data=ordered 0 0
cd /data/local/tmp/
time dd if=/dev/zero of=out bs=4096 count=262144
262144+0 records in
262144+0 records out
1073741824 bytes transferred in 104.006 secs (10,323,845 bytes/sec)
1m44.02s real 0m0.45s user 0m8.42s system
rm out
time dd if=/dev/zero of=out bs=4096 count=262144
…
rm out
time dd if=/dev/zero of=out bs=4096 count=262144
…
time dd if=out of=/dev/null bs=4096 count=262144
262144+0 records in
262144+0 records out
1073741824 bytes transferred in 45.692 secs (23499558 bytes/sec)
0m45.70s real 0m0.54s user 0m8.84s system
time dd if=out of=/dev/null bs=4096 count=262144
…
time dd if=out of=/dev/null bs=4096 count=262144
…
rm out
$ adb shell mount |grep /data /dev/block/dm-0 /data ext4 rw,nosuid,nodev,noatime,barrier=1,data=ordered 0 0
|
|
Breaks during 3.0 to 3.1 OS upgrade. A 3.0-encrypted device had to be master-reset (i.e. all data on /data had to be wiped) on upgrading to 3.1. |
|
|
While Honeycomb’s whole-disk encryption based on dm-crypt is clearly a step in the right direction, it is far from being a NIST FIPS 140-2-certified solution, which requires two-factor encryption and is mandated for most of DOD applications. |
|
|
Apple’s iOS 4 256-bit hardware encryption was cracked in May 2011 by ElcomSoft through a "simple" brute-force attack in as little as 30 mins using CPU and GPUs of modern host machines. See http://www.geek.com/articles/chips/apples-ios-4-hardware-encryption-has-been-cracked-20110525/ for more info. Also, according to Nguyen from Symantec, iOS encryption key is stored on the device, but itself is not encrypted by the user’s master key. This means that if a potential attackers successfully jailbreak the device, they would be able to access the data without knowing the passcode. |
Exploit a vulnerability of the system to give us root once (see below)
Remount the /system partition read-write:
$ mount -o remount,rw -t ext4 /dev/block/mmcblk0p1 /system
Create a setuid version of /system/bin/sh
$ cat /system/bin/sh > /system/bin/su $ chmod 06755 /system/bin/su
Remount the /system partition read-only
$ mount -o remount,ro -t ext4 /dev/block/mmcblk0p1 /system
Use /system/bin/su to become root at any time afterwards
The su binary first checks whether the calling process is white-listed
First, it checks in the SQLite database owned by the Superuser application: /data/data/com.koushikdutta.superuser/databases/superuser.sqlite
If the calling process is already not-approved su exits
If the calling process is not already approved,
su launches a simple activity dialog of the Superuser application by passing the calling process' UID and PID as extra intent parameters:
… char sysCmd[1024]; sprintf(sysCmd, "am start -a android.intent.action.MAIN -n com.koushikdutta.superuser/com.koushikdutta.superuser.SuperuserRequestActivity --ei uid %d --ei pid %d > /dev/null", g_puid, ppid); if (system(sysCmd)) …
Now com.koushikdutta.superuser application starts and SuperuserRequestActivity prompts the user to make their selection
The user’s selection is saved into the database and SuperuserRequestActivity terminates
su now checks the database again, and if the process is not approved, it exits
su then setuid(uid)-s and setgid(gid)-s the calling process
su finally executes /system/bin/sh with the same parameters that the original su was invoked with
The 3rd party application can now use sh to pass any commands that it wishes to run as root
Now, let take a look at some of the past Android exploits (Getting root the first time):
The user copies to the device (e.g. adb push exploid2 /data/local/tmp/exploid2)
The user then runs the "exploid2" process (e.g. adb shell /data/local/tmp/exploid2)
On the first run, the "exploid2" copies itself to /sqlite_stmt_journals/exploid2
The "exploid2" then sends a NETLINK_KOBJECT_UEVENT message (via a local unix socket) to init (i.e., udev code within init) to tell it to run a copy of itself next time a device is plugged in (basically it presents itself as FIRMWARE update for this device)
The user then "hot-plugs" a device by clicking Settings → Wireless → Airplane, WiFi, etc. or plugs in a USB device (if USB host port is available)
The "exploid2" runs again, this time as root (as part of init) and it then
Remounts the /system in read-write mode
Copies itself to /system/bin/rootshell and sets its permission as 04711 (i.e. setuid-bit enabled)
If the user now wants root, the user simply runs /system/bin/rootshell, which then
Switches to root via a simple setuid(0); setgid(0);
Executes /system/bin/sh (now as root) with the parameters passed to /system/bin/rootshell
Changes protections of shared (ashmem) memory space where system properties are stored to allow writing
Sets ro.secure to 0
User restarts adbd
User get root via adb shell
Disables access to shared (ashmem) memory space where system properties are stored (sets protection mask to 0)
User restarts adbd, but since adbdcannot read ro.secure it assumes ro.secure=0
User get root via adb shell
$ adb push mempodroid /data/local/tmp/. $ adb shell chmod 755 /data/local/tmp/mempodroid $ adb shell shell@android:/ # id uid=2000(shell) gid=2000(shell) groups=1003(graphics),1004(input),1007(log),1009(mount),1011(adb),1015(sdcard_rw),3001(net_bt_admin),3002(net_bt),3003(inet) 127|shell@android:/ /data/local/tmp/mempodroid 0xd7f4 0xad4b sh shell@android:/ # id uid=0(root) gid=0(root) groups=1003(graphics),1004(input),1007(log),1009(mount),1011(adb),1015(sdcard_rw),3001(net_bt_admin),3002(net_bt),3003(inet)
Most of the following comes from Android’s own documentation on Memory Management Security Enhancements.
|
|
PIC generally comes with a slight performance penalty on architectures with limited GPRs (like 32-bit x86). |
|
|
PIE static executables are not supported in this release. |
|
|
See Exploit Mitigations in Android Jelly Bean 4.1 for more detailed explanation of the changes that went into 4.1 |
A malicious application starts a "sensitive" activity from another application (e.g. system settings)
The malicious application then overlays a customized notification dialog on top of the legitimate activity (say something that looks like a game)
User interacts with the custom notification dialog, but user touch events are passed down to the legitimate activity (say to inadvertently enable some "insecure" settings mode: like side-loading)
A malicious application starts a background service
User launches a legitimate "secure" application (say a banking app)
This service launches another activity, masquerading as the original
User enters password/pin/gesture to access the "secure" application
The malicious activity captures user input, records it, and simulates a failure - leading the user to re-enter the info into the legitimate activity
The secure app has no knowledge of the fact that the user taps were recorded
The Android Device Administration API, introduced in Android 2.2, allows you to create security-aware applications that are useful in enterprise settings, such as:
You use the Device Administration API to write device admin applications that users install on their devices. The device admin application enforces desired security policies. Here’s how it works:
When enabled, in addition to enforcing security policies, the admin application can:
If a device contains multiple enabled admin applications, the strictest policy is enforced.
If users do not enable the device admin app, it remains on the device, but in an inactive state.
If a user fails to comply with the policies (for example, if a user sets a password that violates the guidelines), it is up to the application to decide how to handle this.
To uninstall an existing device admin application, users need to first deactivate the application as a device administrator.
An admin application may enforce security policies regarding the device’s screen lock PIN/password, including:
Additionally, a security policy can require device storage encryption as of Android 3.0 and disabling of camera as for Android 4.0.
The Device Administration API includes the following classes:
Base class for implementing a device administration component. This class provides a convenience for interpreting the raw intent actions that are sent by the system. Your Device Administration application must include a DeviceAdminReceiver subclass.
A class for managing policies enforced on a device. Most clients of this class must have published a DeviceAdminReceiver that the user has currently enabled. The DevicePolicyManager manages policies for one or more DeviceAdminReceiver instances.
This class is used to specify metadata for a device administrator component.
The manifest of your admin application must register your DeviceAdminReceiver as a <receiver>.
The <receiver> should set android:permission="android.permission.BIND_DEVICE_ADMIN" to ensure that only the system is allowed to interact with the broadcast receiver.
The <receiver> must have an <intent-filter> child element including one or more of the following <action>s, as defined in the DeviceAdminReceiver class:
(Required) This is the primary action that a device administrator must implement to be allowed to manage a device. This is sent to a device administrator when the user enables it for administration.
Action sent to a device administrator when the user has requested to disable it, but before this has actually been done.
Action sent to a device administrator when the user has disabled it.
Action sent to a device administrator when the user has changed the password of their device.
Action periodically sent to a device administrator when the device password is expiring.
Action sent to a device administrator when the user has failed at attempted to enter the password.
Action sent to a device administrator when the user has successfully entered their password, after failing one or more times.
<receiver android:name="MyDeviceAdminReceiver" android:permission="android.permission.BIND_DEVICE_ADMIN"> <intent-filter> <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" /> <action android:name="android.app.action.ACTION_DEVICE_ADMIN_DISABLE_REQUESTED" /> <action android:name="android.app.action.ACTION_DEVICE_ADMIN_DISABLED" /> </intent-filter> <!-- ... --> </receiver>
Your <receiver> element must also include a <meta-data> child element specifying an XML resource declaring the policies used by your admin application.
<meta-data android:name="android.app.device_admin" android:resource="@xml/device_admin_sample" />
An example XML resource requesting all policies would be:
<device-admin xmlns:android="http://schemas.android.com/apk/res/android"> <uses-policies> <limit-password /> <watch-login /> <reset-password /> <force-lock /> <wipe-data /> <expire-password /> <encrypted-storage /> <disable-camera /> </uses-policies> </device-admin>
Your application needs to list only those policies it actually uses.
The DeviceAdminReceiver class defines a set of methods that you can override to handle the device administration events broadcast by the system:
Called after the administrator is first enabled, as a result of receiving ACTION_DEVICE_ADMIN_ENABLED. At this point you can use DevicePolicyManager to set your desired policies.
Called when the user has asked to disable the administrator, as a result of receiving ACTION_DEVICE_ADMIN_DISABLE_REQUESTED. You may return a warning message to display to the user before being disabled, or null for no message.
Called prior to the administrator being disabled, as a result of receiving ACTION_DEVICE_ADMIN_DISABLED. Upon return, you can no longer use the protected parts of the DevicePolicyManager API.
Called after the user has changed their password, as a result of receiving ACTION_PASSWORD_CHANGED.
Called periodically when the password is about to expire or has expired, as a result of receiving ACTION_PASSWORD_EXPIRING. (API 11)
Called after the user has failed at entering their current password, as a result of receiving ACTION_PASSWORD_FAILED.
Called after the user has succeeded at entering their current password, as a result of receiving ACTION_PASSWORD_SUCCEEDED.
You can query the DevicePolicyManager to test if your admin application is enabled:
DevicePolicyManager devicePolicyManager
= (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
ComponentName deviceAdminComponentName
= new ComponentName(this, MyDeviceAdminReceiver.class);
boolean isActive = devicePolicyManager.isAdminActive(deviceAdminComponentName);You could then enable or disable features of your application depending on whether it is an active device administrator.
Your application must explicitly request the user to enable it for device administration. To do so:
Create an implicit Intent with the DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN action:
Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
Add an extra identifying your DeviceAdminReceiver component:
ComponentName deviceAdminComponentName
= new ComponentName(this, MyDeviceAdminReceiver.class);
intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, deviceAdminComponentName);Optionally, provide an explanation as to why the user should activate the admin application:
intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, "Your boss told you to do this");
Use the Intent with startActivityForResult() to display the activation dialog:
startActivityForResult(intent, ACTIVATION_REQUEST);
You can test for successful activation in your Activity’s onActivityResult() method:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case ACTIVATION_REQUEST: if (resultCode == Activity.RESULT_OK) { Log.i("DeviceAdminSample", "Administration enabled!"); } else { Log.i("DeviceAdminSample", "Administration enable FAILED!"); } return; } super.onActivityResult(requestCode, resultCode, data); }
DevicePolicyManager includes APIs for setting and enforcing the device screen lock password policy.
The user must enter a password containing at least alphabetic (or other symbol) characters.
The user must enter a password containing at least both numeric and alphabetic (or other symbol) characters.
The user must enter a password containing at least numeric characters.
The policy requires some kind of password, but doesn’t care what it is.
The policy has no requirements for the password.
(API 11) The user must have entered a password containing at least a letter, a numerical digit and a special symbol.
devicePolicyManager.setPasswordQuality(deviceAdminComponentName, PASSWORD_QUALITY_ALPHANUMERIC); devicePolicyManager.setPasswordMinimumLength(deviceAdminComponentName, 6);
Your application’s policy metadata resource must request the <limit-password /> policy to control password quality; otherwise these methods throw a security exception.
Beginning with Android 3.0, the DevicePolicyManager class includes methods that give you greater control over the contents of the password. Here are the methods for fine-tuning a password’s contents:
You can also set the password expiration timeout, and prevent users from reusing the last n unique passwords:
Additionally, Android 3.0 introduced support for a policy requiring the user to encrypt the device, which you can set with:
Your application’s policy metadata resource must request the <limit-password /> policy to control password quality; otherwise these methods throw a security exception.
Similarly, it must request the <expire-password /> and <encrypted-storage /> policies to control those features without throwing a security exception.
You can test if the current device password meets the quality requirements by calling DevicePolicyManager.isActivePasswordSufficient(), which returns a boolean result.
If necessary, you can start an activity prompting the user to set a password as follows:
Intent intent = new Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD); startActivity(intent);
Your application can also perform a password reset on the device using DevicePolicyManager.resetPassword(). This can be useful if your application is designed to support remote administration, with a new password being provided from a central administration system.
Your application’s policy metadata resource must request the <reset-password /> policy to reset the password; otherwise resetPassword() throws a security exception.
Your application can lock the device programmatically using DevicePolicyManager.lockNow().
You can wipe the user data of the device, performing a factory reset, using DevicePolicyManager.wipeData().
Additionally, you can set the maximum number of allowed failed password attempts before the device is wiped automatically by calling DevicePolicyManager.setMaximumFailedPasswordsForWipe()
Your application’s policy metadata resource must request the <wipe-data /> policy to wipe the data either explicitly or set the maximum failed passwords for wipe; otherwise a security exception is thrown. setMaximumFailedPasswordsForWipe() also requires the <watch-login /> policy.
The lockNow() method requires your application to request the <force-lock /> policy to avoid throwing a security exception.
In this example app, you will see how to write an application that requests to device administration privileges, and once it gets them, allows user to lock or reset the device.
We are going to look at the following files:
The source code for this project is available at https://marakana.com/static/courseware/android/DevicePolicyDemo.zip
This is where we register our device administration receiver component. It appears as another receiver declaration.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.devicepolicydemo" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="10" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".DevicePolicyDemoActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- This is where we register our receiver --> <receiver android:name=".DemoDeviceAdminReceiver" android:permission="android.permission.BIND_DEVICE_ADMIN" > <intent-filter> <!-- This action is required --> <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" /> </intent-filter> <!-- This is required this receiver to become device admin component. --> <meta-data android:name="android.app.device_admin" android:resource="@xml/device_admin_sample" /> </receiver> </application> </manifest>
Notice that <receiver> element now includes required android:permission="android.permission.BIND_DEVICE_ADMIN" permission declaration.
We also must include the appropriate intent action filter android.app.action.DEVICE_ADMIN_ENABLED as well as the <meta-data/> element that specifies that this receiver users @xml/device_admin_sample resource, which we’ll look at next.
This XML resource file, referenced from AndroidManifest.xml specifies what policies we are interested in.
<device-admin xmlns:android="http://schemas.android.com/apk/res/android"> <uses-policies> <limit-password /> <watch-login /> <reset-password /> <force-lock /> <wipe-data /> <expire-password /> <encrypted-storage /> </uses-policies> </device-admin>
In this example, we ask for most of the available policies merely to illustrate what is available. In a real-world example, you should only ask for policies that you really require.
This is the main device administration component. It is basically a specialized BroadcastReceiver class that implements some callbacks specific to device administration.
package com.marakana.android.devicepolicydemo; import android.app.admin.DeviceAdminReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; import android.widget.Toast; /** * This is the component that is responsible for actual device administration. * It becomes the receiver when a policy is applied. It is important that we * subclass DeviceAdminReceiver class here and to implement its only required * method onEnabled(). */ public class DemoDeviceAdminReceiver extends DeviceAdminReceiver { static final String TAG = "DemoDeviceAdminReceiver"; /** Called when this application is approved to be a device administrator. */ @Override public void onEnabled(Context context, Intent intent) { super.onEnabled(context, intent); Toast.makeText(context, R.string.device_admin_enabled, Toast.LENGTH_LONG).show(); Log.d(TAG, "onEnabled"); } /** Called when this application is no longer the device administrator. */ @Override public void onDisabled(Context context, Intent intent) { super.onDisabled(context, intent); Toast.makeText(context, R.string.device_admin_disabled, Toast.LENGTH_LONG).show(); Log.d(TAG, "onDisabled"); } @Override public void onPasswordChanged(Context context, Intent intent) { super.onPasswordChanged(context, intent); Log.d(TAG, "onPasswordChanged"); } @Override public void onPasswordFailed(Context context, Intent intent) { super.onPasswordFailed(context, intent); Log.d(TAG, "onPasswordFailed"); } @Override public void onPasswordSucceeded(Context context, Intent intent) { super.onPasswordSucceeded(context, intent); Log.d(TAG, "onPasswordSucceeded"); } }
Notice that we subclass DeviceAdminReceiver class. This is the required for this component to be able to receive policy notifications.
We also must implement the required onEnabled() method that is called when the policy administration is first enabled.
We don’t really do much here other than log what happened to visually illustrate the execution of this code.
The activity acts as our demo client in this case. The significant methods are onClick(), onCheckedChanged() and onActivityResult().
package com.marakana.android.devicepolicydemo; import android.app.Activity; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.Toast; import android.widget.ToggleButton; public class DevicePolicyDemoActivity extends Activity implements OnCheckedChangeListener { static final String TAG = "DevicePolicyDemoActivity"; static final int ACTIVATION_REQUEST = 47; // identifies our request id DevicePolicyManager devicePolicyManager; ComponentName demoDeviceAdmin; ToggleButton toggleButton; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); toggleButton = (ToggleButton) super .findViewById(R.id.toggle_device_admin); toggleButton.setOnCheckedChangeListener(this); // Initialize Device Policy Manager service and our receiver class devicePolicyManager = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE); demoDeviceAdmin = new ComponentName(this, DemoDeviceAdminReceiver.class); } /** * Called when a button is clicked on. We have Lock Device and Reset Device * buttons that could invoke this method. */ public void onClick(View v) { switch (v.getId()) { case R.id.button_lock_device: // We lock the screen Toast.makeText(this, "Locking device...", Toast.LENGTH_LONG).show(); Log.d(TAG, "Locking device now"); devicePolicyManager.lockNow(); break; case R.id.button_reset_device: // We reset the device - this will erase entire /data partition! Toast.makeText(this, "Locking device...", Toast.LENGTH_LONG).show(); Log.d(TAG, "RESETing device now - all user data will be ERASED to factory settings"); devicePolicyManager.wipeData(ACTIVATION_REQUEST); break; } } /** * Called when the state of toggle button changes. In this case, we send an * intent to activate the device policy administration. */ @Override public void onCheckedChanged(CompoundButton button, boolean isChecked) { if (isChecked) { // Activate device administration Intent intent = new Intent( DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN); intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, demoDeviceAdmin); intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION, "Your boss told you to do this"); startActivityForResult(intent, ACTIVATION_REQUEST); } Log.d(TAG, "onCheckedChanged to: " + isChecked); } /** * Called when startActivityForResult() call is completed. The result of * activation could be success of failure, mostly depending on user okaying * this app's request to administer the device. */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case ACTIVATION_REQUEST: if (resultCode == Activity.RESULT_OK) { Log.i(TAG, "Administration enabled!"); toggleButton.setChecked(true); } else { Log.i(TAG, "Administration enable FAILED!"); toggleButton.setChecked(false); } return; } super.onActivityResult(requestCode, resultCode, data); } }
onCheckedChanged() is invoked when the toggle button changes state. It sends an intent requesting that this application be granted device administration permissions on this device. User has to allow this request. The result of user’s action is then passed to onActivityResult() method.
onClick() method processes button clicks for lock and reset buttons. Note that its calls to DevicePolicyManager will lock and wipe out the device user data partition, respectively.
Device-admin-enable an existing Android app:
Download http://marakana.com/static/courseware/android/SecureNote.zip
Expand into your (Eclipse) workspace
Import into Eclipse
Using Device Admin APIs
Require that the screen-lock password be set (of at least 6 characters with at least one digit)
Automatically wipe the device after 5 invalid login attempts
Check against the solution available at http://marakana.com/static/courseware/android/DeviceAdministeredSecureNote.zip
DexClassLoader classLoader = new DexClassLoader( new File(context.getFilesDir(), "bad.jar").getAbsolutePath(), // contains classes.dex context.getDir("dex", 0).getAbsolutePath(), // create a directory for optimized DEX code null, // we could also specify a path for native libraries! context.getClassLoader()); Class<?> badClass = classLoader.loadClass("com.malicious.app.Bad"); Bad bad = (Bad) badClass.newInstance(); bad.run();
for (PackageInfo packageInfo : getPackageManager().getInstalledPackages(0)) { if (packageInfo.packageName.startsWith("com.malware")) { // found malware! } }
for (PackageInfo packageInfo : getPackageManager().getInstalledPackages(PackageManager.GET_PERMISSIONS)) { if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { continue; // skip system apps } if (packageInfo.requestedPermissions != null) { for (String requestedPermission : packageInfo.requestedPermissions) { if (requestedPermission.equals(android.Manifest.permission.SEND_SMS)) { // found malware! } } } }
CertificateFactory certificateFactory = CertificateFactory.getInstance("X509"); PublicKey trustedIssuerPublicKey = // get some trusted key for (PackageInfo packageInfo : getPackageManager().getInstalledPackages(PackageManager.GET_SIGNATURES)) { if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { continue; // skip system apps } if (packageInfo.signatures != null) { for (Signature signature : packageInfo.signatures) { InputStream input = new ByteArrayInputStream(signature.toByteArray()); try { X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(input); cert.checkValidity(); cert.verify(trustedIssuerPublicKey); if (cert.getIssuerDN().getName().equals("CN=Some One,O=Bad,C=US")) { // found malware! } } catch (CertificateException e) { // found malware! } } } }
for (PackageInfo packageInfo : getPackageManager().getInstalledPackages(0)) { if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { continue; // skip system apps } String apk = packageInfo.applicationInfo.sourceDir; FileInputStream in = new FileInputStream(apk); // this is legal! try { // scan for malicious code (unzip first) or upload to remote server for analysis } finally { in.close(); } String nativeDir = packageInfo.applicationInfo.nativeLibraryDir; // these are readable by all! // scan native libraries for malicious code or upload to remote server for analysis }
<manifest … > … <application … > … <receiver android:name=".ApplicationInstallReceiver" > <intent-filter> <action android:name="android.intent.action.PACKAGE_ADDED" /> <action android:name="android.intent.action.PACKAGE_REPLACED" /> <data android:scheme="package" /> </intent-filter> </receiver> </application> </manifest>
… public class ApplicationInstallReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Uri packageUri = intent.getData(); String packageName = packageUri.getEncodedSchemeSpecificPart(); int flags = PackageManager.GET_PERMISSIONS | PackageManager.GET_SIGNATURES; try { PackageInfo packageInfo = context.getPackageManager().getPackageInfo(packageName, flags); // verify packageInfo } catch (NameNotFoundException e) { // handle } } }
Uri packageURI = Uri.parse("package:com.malicous.app"); Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageURI); startActivity(uninstallIntent);
What is the basic philosophy of the Android security model?
What does Android do to implement this philosophy?
How and when are permissions used, granted, and enforced?
What is a confused deputy attack?
What is a collusion attack?
Which part of Android is ultimately responsible for providing the system security?
How does native code differ from Java code when it comes to its security context?
Why do we sign applications?
What are the four platform keys?
What is the relationship between apps and Linux user IDs?
When are the user IDs assigned to apps?
Where are the user ID assignments recorded?
What is the relationship between application user IDs and its file-system resources?
Where is the home of an app with the package name com.foo.bar?
How do apps share a user IDs?
How do apps share a process? Provide at least one use-case for this.
Name at least five (file) system volumes (think mount points).
What is the purpose of /data/dalvik-cache/?
What is the purpose of run-as command?
How do you use permissions?
What happens if you don’t use a permission but you attempt to use a restricted API?
How are the permissions enforced?
How do bound services enforce their permissions? What makes that possible?
What are the attributes of the <permission … /> tag?
What are the possible values for protectionLevel attribute?
What does protectedLevel=signatureOrSystem mean?
What is the value of grouping permissions?
What is the significance of android:grantUriPermission attribute on content providers?
What is the significance android:exported attribute on Android components?
How do you protected unauthorized applications from receiving your broadcasts (intents)?
What’s the worry with pending intents?
What is the purpose of data encryption and when is it useful?
What is CBC?
What is IV?
What is the "whole disk encryption"?
What can we do with VPN on ICS?
How can we manage keys on ICS?
What are the dangers of rooting?
How do we get root?
How can we "test" for root?
What is ALSR? And what is its purpose?
What is NX?
What is tap jacking?
What is the purpose of Device Admin API? What can you do with it?
What are the ways we can identify malware? When can we do this?
What is DAC? What is MAC? And what can we do with SE-Android?
Add the following to:
… * soft nofile 8192 * hard nofile 8192
Reboot
$ mkdir ~/bin $ export PATH=~/bin:$PATH $ curl https://dl-ssl.google.com/dl/googlesource/git-repo/repo > ~/bin/repo $ chmod a+x ~/bin/repo
$ mkdir android-src $ cd android-src
$ repo init -u https://android.googlesource.com/platform/manifest.git -b android-4.1.1_r1
$ git clone https://android.googlesource.com/platform/manifest.git $ cd manifest $ git branch -a
|
|
Official Android repository android.googlesource.com is not the only game in town. For example, to get Android sources for TI’s SOCs, we would: repo init -u git://gitorious.org/rowboat/manifest.git -m TI-Android-FroYo-DevKit-V2.2.xml for OMAP3 and repo init -u git://gitorious.org/rowboat/manifest.git -m TI-Android-FroYo-DSP-DevKit-V2.xml for DM37x EVM (the .xml files come from TI) to check out Froyo build optimized for their hardware. |
$ repo sync
$ source build/envsetup.sh $ help Invoke ". build/envsetup.sh" from your shell to add the following functions to your environment: - croot: Changes directory to the top of the tree. - m: Makes from the top of the tree. - mm: Builds all of the modules in the current directory. - mmm: Builds all of the modules in the supplied directories. - cgrep: Greps on all local C/C++ files. - jgrep: Greps on all local Java files. - resgrep: Greps on all local res/*.xml files. - godir: Go to the directory containing a file. Look at the source to view more functions. The complete list is: add_lunch_combo cgrep check_product check_variant choosecombo chooseproduct choosetype choosevariant cproj croot findmakefile gdbclient get_abs_build_var get_build_var getbugreports getprebuilt gettop godir help isviewserverstarted jgrep lunch m mm mmm pid print_lunch_menu printconfig resgrep runhat runtest set_java_home set_sequence_number set_stuff_for_environment setpaths settitle smoketest startviewserver stopviewserver systemstack tapas tracedmdump
$ lunch
You're building on linux
Lunch menu... pick a combo:
1. generic-eng
2. full_passion-userdebug
3. full_crespo-userdebug
4. full_crespo4g-userdebug
Which would you like? [generic-eng]
============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=2.3.4
TARGET_PRODUCT=generic
TARGET_BUILD_VARIANT=eng
TARGET_SIMULATOR=false
TARGET_BUILD_TYPE=release
TARGET_BUILD_APPS=
TARGET_ARCH=arm
HOST_ARCH=x86
HOST_OS=linux
HOST_BUILD_TYPE=release
BUILD_ID=GINGERBREAD
============================================
$ choosecombo
Only device builds are supported for linux
Forcing TARGET_SIMULATOR=false
Press enter:
Build type choices are:
1. release
2. debug
Which would you like? [1]
Which product would you like? [generic]
Variant choices are:
1. user
2. userdebug
3. eng
Which would you like? [eng]
============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=2.3.4
TARGET_PRODUCT=generic
TARGET_BUILD_VARIANT=eng
TARGET_SIMULATOR=false
TARGET_BUILD_TYPE=release
TARGET_BUILD_APPS=
TARGET_ARCH=arm
HOST_ARCH=x86
HOST_OS=linux
HOST_BUILD_TYPE=release
BUILD_ID=GINGERBREAD
============================================
|
|
Building AOSP on Linux-x86 also supported a simulator target, which was a partial build of Android, compiled as a single Linux x86 binary, meant to run as a stand-alone process. It would enable testing Dalvik, OpenCORE/Stagefright, or WebKit under Valgrind (instrumentation/debugging framework). But simulator has been dropped as a valid lunch target since it’s no longer actively maintained by the AOSP team. |
|
|
Additional CodeName-BuildType combinations may be available based on what build/envsetup.sh finds (usually in the device/ folder) |
|
|
Building for real hardware usually requires that we get proprietary binaries (mostly user-space HAL). For Nexus and other Google-supported devices, we can go to http://code.google.com/android/nexus/drivers.html, which allows us to download scripts, which in turn extract binaries from the connected devices (via adb pull) into vendor/ directory tree structure. |
$ export USE_CCACHE=1
$ prebuilt/linux-x86/ccache/ccache -M 50G
$ make -j4
|
|
GNU Make supports -jN argument, which allows us to parallelize compilation across multiple hardware threads. Typically, N should be set to the number of hardware threads available on the host machine plus two. For example, use make -j10 to build on a quad-core i7 with two hardware threads per core (i.e. hyperthreaded). |
|
|
Use -jN with caution, because you may run into memory, disk I/O, or max open-file limits. |
Android build system Makefile (actually build/core/main.mk) includes some of the following targets:
| Make targets | Description |
|---|---|
droid |
The default target (build the full system) |
clean |
Equivalent to rm -rf out/ (same as make clobber) |
installclean |
Deletes all of the files that change between different build types, like make user vs. make sdk |
dataclean |
Delete files in the staging and emulator data partitions: data/*, data-qemu/*, and userdata-qemu.img |
snod |
Quickly rebuild the system image from built packages |
offline-sdk-docs |
Generate the HTML for the developer SDK docs |
doc-comment-check-docs |
Check HTML doc links and validity, without generating HTML |
libandroid_runtime |
All the JNI framework stuff |
framework |
All the java framework stuff |
services |
The system server (Java) and friends |
sdk |
Build the Android SDK (tools) |
help |
Display a help message listing some of these targets |
modules |
Display a list of modules that can be built (where each module name is specified by LOCAL_MODULE) |
<module-name> |
Make just a specific module (same as cd module/dir && mm) |
clean-<module-name> |
Clean just a specific module |
otacerts |
OTA keys that are used to verify OTA packages |
… |
Etc. |
52 /* 53 ** +-----------------+ 54 ** | boot header | 1 page 55 ** +-----------------+ 56 ** | kernel | n pages 57 ** +-----------------+ 58 ** | ramdisk | m pages 59 ** +-----------------+ 60 ** | second stage | o pages 61 ** +-----------------+ 62 ** 63 ** n = (kernel_size + page_size - 1) / page_size 64 ** m = (ramdisk_size + page_size - 1) / page_size 65 ** o = (second_size + page_size - 1) / page_size 66 ** 67 ** 0. all entities are page_size aligned in flash 68 ** 1. kernel and ramdisk are required (size != 0) 69 ** 2. second is optional (second_size == 0 -> no second) 70 ** 3. load each element (kernel, ramdisk, second) at 71 ** the specified physical address (kernel_addr, etc) 72 ** 4. prepare tags at tag_addr. kernel_args[] is 73 ** appended to the kernel commandline in the tags. 74 ** 5. r0 = 0, r1 = MACHINE_TYPE, r2 = tags_addr 75 ** 6. if second_size != 0: jump to second_addr 76 ** else: jump to kernel_addr 77 */
$ source build/envsetup.sh $ lunch … $ make … $ out/host/linux-x86/bin/emulator &
|
|
Replace linux-x86 with darwin-x86 for Mac OS X. |
$ android create avd --sdcard 16M --target android-10 --name custom-avd --path custom-avd Android 2.3.3 is a basic Android platform. Do you wish to create a custom hardware profile [no] Created AVD 'custom-avd' based on Android 2.3.3, with the following hardware config: hw.lcd.density=240 vm.heapSize=24 hw.ramSize=256
$ emulator -avd custom-avd -system out/target/product/generic/system.img -ramdisk out/target/product/generic/ramdisk.img &
$ adb reboot bootloader
$ out/host/linux-x86/bin/fastboot oem unlock
|
|
Nexus One (passion) does not allow the bootloader to be locked again with fastboot oem lock |
$ out/host/linux-x86/bin/fastboot flashall -w
|
|
Running this command will wipe our device! The -w switch is used to also wipe the /data partition, which is sometimes necessary (on first-flash, or on major updates), but is otherwise not required. |
|
|
Nexus One (passion) does not support the -w switch, so instead we can fastboot erase cache and fastboot erase userdata before flashing. |
|
|
It is easiest to build the Linux kernel on a Linux OS. While other host OSs can also be used, they are not trivial to setup. |
$ git clone https://android.googlesource.com/kernel/common.git $ git clone https://android.googlesource.com/kernel/goldfish.git $ git clone https://android.googlesource.com/kernel/msm.git $ git clone https://android.googlesource.com/kernel/omap.git $ git clone https://android.googlesource.com/kernel/samsung.git $ git clone https://android.googlesource.com/kernel/tegra.git
Start the emulator
Get the existing kernel version from /proc/version (since uname does not exist on Android)
$ adb shell cat /proc/version Linux version 2.6.29-g46b05b2 (vchtchetkine@vc-irv.irv.corp.google.com) (gcc version 4.4.3 (GCC) ) #28 Thu Nov 17 06:39:36 PST 2011
Get the corresponding kernel version (here, we are getting the kernel for goldfish, the emulator)
$ git clone https://android.googlesource.com/kernel/goldfish.git $ cd goldfish/ $ git branch -a $ git checkout -t remotes/origin/android-goldfish-2.6.29
|
|
Alternatively, we could directly clone the goldfish 2.6.29 branch $ git clone https://android.googlesource.com/kernel/goldfish.git -b android-goldfish-2.6.29 |
Specify the target architecture and cross compiler
$ export ARCH=arm $ export CROSS_COMPILE=/path/to/android-src/prebuilt/linux-x86/toolchain/arm-eabi-4.4.3/bin/arm-eabi-
|
|
Android’s own build system uses ARCH and CROSS_COMPILE env vars, so these need to be cleared before building AOSP. As an alternative, these can be also passed directly to the make commands: $ make … ARCH=… CROSS_COMPILE=… |
Get the existing kernel configuration file (with all of the Android/emulator specific options) by pulling it from the running emulator:
$ adb pull /proc/config.gz . $ gunzip config.gz $ mv config .config
|
|
As an alternative, we could generate the Goldfish .config file (even without the emulator) by running: $ make goldfish_armv7_defconfig ARCH=arm |
You can optionally take a look at the existing configuration options and make changes as desired
$ make menuconfig ARCH=arm
Now we are ready to compile
$ make
The resulting kernel will be compressed to arch/arm/boot/zImage
Run the emulator with our new kernel
$ out/host/linux-x86/bin/emulator -kernel /path/to/common/arch/arm/boot/zImage
And the result is
What is repo?
What Android’s build system based on?
What is the first thing you need to do before you can build Android (after you get the source)?
What is the unit of build?
What are the different build types and how do they differ?
What is the purpose of ccache?
What is the end result of a build?
What’s inside recovery.img?
What is the purpose of fastboot?
What is the purpose of CROSS_COMPILE variable and what do you set it to when compiling the kernel?
On power-up, CPU is uninitialized - wait for stable power
Execute Boot ROM (hardwired into CPU)
Locate the first-stage boot loader
Load the first-stage boot loader into internal RAM
Jump to first-stage boot loader’s memory location to execute it
First-stage boot loader runs
Detect and initialize external RAM
Locate the second-stage boot loader
Load the second-stage boot loader into external RAM
Jump to the second-stage boot loader’s memory location to execute it
Second-stage boot loader runs
Setup file systems (typically on Flash media)
Optionally setup display, network, additional memory, and other devices
Enable additional CPU features
Enable low-level memory protection
Optionally load security protections (e.g. ROM validation code)
Locate Linux Kernel
Load Linux Kernel into RAM
Place Linux Kernel boot parameters into memory so that kernel knows what to run upon startup
Jump to Linux Kernel memory address to run it
Linux Kernel runs
Build a table in RAM describing the layout of the physical memory
Initialize and setup input devices
Initialize and setup disk (typically MTD) controllers and map available block devices in RAM
Initialize Advanced Power Management (APM) support
Initialize interrupt handlers: Interrupt Descriptor Table (IDT), Global Descriptor Table (GDT), and Programmable Interrupt Controllers (PIC)
Reset the floating-point unit (FPU)
Switch from real to protected mode (i.e. enable memory protection)
Initialize segmentation registers and a provisional stack
Zero uninitialized memory
Decompress the kernel image
Initialize provisional kernel page tables and enable paging
Setup kernel mode stack for process 0
Fill the IDT with null interrupt handlers
Initialize the first page frame with system parameters
Identify the CPU model
Initialize registers with the addresses of the GDT and IDT
Initialize and start the kernel
Scheduler
Memory zones
Buddy system allocator
IDT
SoftIRQs
Date and Time
Slab allocator
…
Create process 1 (/init) and run it
on <trigger> <command> <command> <command>
service <name> <pathname> [ <argument> ]* <option> <option> ...
Starts ueventd
Initializes the system clock and logger
Sets up global environment
Sets up the file system (mount points and symbolic links)
Configures kernel timeouts and scheduler
Configures process groups
Mounts the file systems
Creates a basic directory structure on /data and applies permissions
Applies permissions on /cache
Applies permissions on certain /proc points
Initializes local network (i.e. localhost)
Configures the parameters for the low memory killer [Android_Linux_Kernel_Low_Memory_Killer]
Applies permissions for system_server and daemons
Defines TCP buffer sizes for various networks
Configures and (optionally) loads various daemons (i.e. services): ueventd, console, adbd, servicemanager, vold, netd, debuggerd, rild, zygote (which in turn starts system_server), mediaserver, bootanimation (one time), and various Bluetooth daemons (like dbus-daemon, bluetoothd, etc.), installd, racoon, mtpd, keystore
Sets up product info
Initializes device-driver-specific info, file system structures, and permissions: battery, wifi, phone, uart_switch, GPS, radio, bluetooth, NFC, lights
Initializes and (re)mounts file systems
Loads additional device-specific daemons
Zygote starts from /init.rc
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
socket zygote stream 666
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
This translates to frameworks/base/cmds/app_process/app_main.cpp:main()
The command app_process then launches frameworks/base/core/java/com/android/internal/os/ZygoteInit.java:main() in a Dalvik VM via frameworks/base/core/jni/AndroidRuntime.cpp:start()
ZygoteInit.main() then
Registers for zygote socket
Pre-loads classes defined in frameworks/base/preloaded-classes (1800+)
Pre-loads resources preloaded_drawables and preloaded_color_state_lists from frameworks/base/core/res/res/values/arrays.xml
Runs garbage collector (to clean the memory as much as possible)
Forks itself to start system_server
Starts listening for requests to fork itself for other apps
When Zygote forks itself to launch the system_server process (in ZygoteInit.java:startSystemServer()), it executes frameworks/base/services/java/com/android/server/SystemServer:java.main()
The SystemServer:java.main() method loads android_servers JNI lib from frameworks/base/services/jni and invokes init1() native method
Before init1() runs, the JNI loader first runs frameworks/base/services/jni/onload.cpp:JNI_OnLoad(), which registers native services - to be used as JNI counterparts to Java-based service manager loaded later
Now frameworks/base/services/jni/com_android_server_SystemServer.cpp:init1() is invoked, which simply wraps a call to frameworks/base/cmds/system_server/library/system_init.cpp:system_init()
The system_init.cpp:system_init() function
First starts native services (some optionally):
frameworks/base/services/surfaceflinger/SurfaceFlinger.cpp
frameworks/base/services/sensorservice/SensorService.cpp
frameworks/base/services/audioflinger/AudioFlinger.cpp
frameworks/base/media/libmediaplayerservice/MediaPlayerService.cpp
frameworks/base/camera/libcameraservice/CameraService.cpp
frameworks/base/services/audioflinger/AudioPolicyService.cpp
Then goes back to frameworks/base/services/java/com/android/server/SystemServer.java:init2(), again via frameworks/base/core/jni/AndroidRuntime.cpp:start() JNI call
The SystemServer.java:init2() method then starts Java service managers in a separate thread (ServerThread), readies them, and registers each one with frameworks/base/core/java/android/os/ServiceManager:addService() (which in turn delegates to to ServiceManagerNative.java, which effectively talks to servicemanager daemon previously started by init)
frameworks/base/services/java/com/android/server/PowerManagerService.java
frameworks/base/services/java/com/android/server/am/ActivityManagerService.java
frameworks/base/services/java/com/android/server/TelephonyRegistry.java
frameworks/base/services/java/com/android/server/PackageManagerService.java
frameworks/base/services/java/com/android/server/BatteryService.java
frameworks/base/services/java/com/android/server/VibratorService.java
etc
Finally frameworks/base/services/java/com/android/server/am/ActivityManagerService.java:finishBooting() sets sys.boot_completed=1 and sends out
a broadcast intent with android.intent.action.PRE_BOOT_COMPLETED action (to give apps a chance to reach to boot upgrades)
an activity intent with android.intent.category.HOME category to launch the Home (or Launcher) application
a broadcast intent with android.intent.action.BOOT_COMPLETED action, which launches applications subscribed to this intent (while using android.permission.RECEIVE_BOOT_COMPLETED)
What is the purpose of the second-stage bootloader?
What is the first thing that runs (in the user-space) after the kernel is initialized?
Where does this process (with PID==1) get its initialization instructions from?
What are actions (of process with PID==1)?
Name at least three triggers and three commands for actions.
What are services (of process with PID==1)?
How are services launched?
What’s the difference between critical and non-critical services?
Name at least five services started by process with PID==1.
What is the purpose of ueventd process?
What is the purpose of zygote process and what does it do upon initialization?
What is the purpose of system_server process and what does it do upon initialization?
Who is responsible for launching the Home application and how is it done?
How do applications that want to start on boot get launched?
The objective of this module is to explain the inter-workings of various Android services. There are close to sixty various services in ICS release. In this module, we’ve hand-picked some of the more common ones. By the end of the module, you should start to understand some common traits of Android service architecture, such as use of the Binder for inter-process communication, and use of JNI for Java-C interaction.
Android services are the key to exposing lower level functionality of the hardware and the Linux kernel to the high level Android apps. Understanding how they work creates the opportunity to customize and extend their behavior, or add another service altogether.
$ adb shell service list Found 56 services: 0 phone: [com.android.internal.telephony.ITelephony] 1 iphonesubinfo: [com.android.internal.telephony.IPhoneSubInfo] 2 simphonebook: [com.android.internal.telephony.IIccPhoneBook] 3 isms: [com.android.internal.telephony.ISms] 4 samplingprofiler: [] 5 diskstats: [] 6 appwidget: [com.android.internal.appwidget.IAppWidgetService] 7 backup: [android.app.backup.IBackupManager] 8 uimode: [android.app.IUiModeManager] 9 usb: [android.hardware.usb.IUsbManager] 10 audio: [android.media.IAudioService] 11 wallpaper: [android.app.IWallpaperManager] 12 dropbox: [com.android.internal.os.IDropBoxManagerService] 13 search: [android.app.ISearchManager] 14 country_detector: [android.location.ICountryDetector] 15 location: [android.location.ILocationManager] 16 devicestoragemonitor: [] 17 notification: [android.app.INotificationManager] 18 mount: [IMountService] 19 throttle: [android.net.IThrottleManager] 20 connectivity: [android.net.IConnectivityManager] 21 wifi: [android.net.wifi.IWifiManager] 22 wifip2p: [android.net.wifi.p2p.IWifiP2pManager] 23 netpolicy: [android.net.INetworkPolicyManager] 24 netstats: [android.net.INetworkStatsService] 25 textservices: [com.android.internal.textservice.ITextServicesManager] 26 network_management: [android.os.INetworkManagementService] 27 clipboard: [android.content.IClipboard] 28 statusbar: [com.android.internal.statusbar.IStatusBarService] 29 device_policy: [android.app.admin.IDevicePolicyManager] 30 accessibility: [android.view.accessibility.IAccessibilityManager] 31 input_method: [com.android.internal.view.IInputMethodManager] 32 window: [android.view.IWindowManager] 33 alarm: [android.app.IAlarmManager] 34 vibrator: [android.os.IVibratorService] 35 battery: [] 36 hardware: [android.os.IHardwareService] 37 content: [android.content.IContentService] 38 account: [android.accounts.IAccountManager] 39 permission: [android.os.IPermissionController] 40 cpuinfo: [] 41 gfxinfo: [] 42 meminfo: [] 43 activity: [android.app.IActivityManager] 44 package: [android.content.pm.IPackageManager] 45 telephony.registry: [com.android.internal.telephony.ITelephonyRegistry] 46 usagestats: [com.android.internal.app.IUsageStats] 47 batteryinfo: [com.android.internal.app.IBatteryStats] 48 power: [android.os.IPowerManager] 49 entropy: [] 50 sensorservice: [android.gui.SensorServer] 51 media.audio_policy: [android.media.IAudioPolicyService] 52 media.camera: [android.hardware.IAudioPolicyServiceService] 53 media.player: [android.media.IMediaPlayerService] 54 media.audio_flinger: [android.media.IAudioFlinger] 55 SurfaceFlinger: [android.ui.ISurfaceComposer]
Wifi Service exposes wifi functionality of the underlying system to the application layer via WifiManager class. The key differentiation between wifi stack and some other ones is that the wifi stack primarily uses the wpa_supplicant to manage the Wifi driver.
Parsing and decoding of audio data by [Android_Media_Framework] (Stagefright) via the mediaplayer service
Writing decoded (PCM) data to an AudioTrack (AudioSink)
Mixing AudioTracks in AudioFlinger’s mixer thread (AudioFlinger::MixerThread::threadLoop())
Applying effects
Buffering stream data
Writing buffers to a PCM output device (e.g. ALSA driver) via android_audio_legacy.AudioHardware (audio legacy HAL) or "audio" hardware module HAL
/system/lib/audioflinger.so
/system/lib/libhardware.so
/system/lib/hw/audio.primary.tuna.so
/system/lib/libtinyalsa.so
/dev/snd/pcmC0D0p
The media framework are the APIs and libraries used for controlling playback and recording of video/audio. Since everything involving video and audio require a lot of computing power, mobile devices usually use a lot of special-purpose hardware for this, compared to desktop computers where there normally is enough raw power to run most/everything in software. Full-software solutions normally are more flexible and portable, but obviously require more processor power (which also might give a higher power consumption).
Normally only the highest level APIs are public, to allow for flexibility in the implementation internally.
java: android.media.MediaPlayer
frameworks/base/media/java/android/media/MediaPlayer.java
JNI: frameworks/base/media/jni/android_media_MediaPlayer.cpp
Quite straight mapping of java native functions to the C++ MediaPlayer class
C++: MediaPlayer
frameworks/base/media/libmedia/mediaplayer.cpp
IMediaPlayer, IMediaPlayerService
frameworks/base/include/media/IMediaPlayer.h
frameworks/base/include/media/IMediaPlayerService.h
BpMediaPlayer: Binder client/proxy interface
frameworks/base/media/libmedia/IMediaPlayer.cpp
BnMediaPlayer: Binder implementation interface (running in the media server)
BnMediaPlayer implemented by MediaPlayerService::Client
frameworks/base/libmediaplayservice/MediaPlayerService.cpp
MediaPlayerService backed by different implementations:
MediaPlayerService::AudioOutput
frameworks/base/libmediaplayservice/MediaPlayerService.cpp
AudioTrack
frameworks/base/media/libmedia/AudioTrack.cpp
IAudioTrack
AudioFlinger::TrackHandle
frameworks/base/services/audioflinger/AudioFlinger.cpp
The actual decoding/encoding in the OMXCodec and ACodec class is implemented via the IOMX interface:
The applications may also use OpenSL ES for playing back audio this internally uses OMXCodec from stagefright for decoding of the audio. (Code for this is in system/media/opensles/libopensles/android SfPlayer.cpp in gingerbread, in system/media/wilhelm/src/android/android AudioSfDecoder.cpp in ICS.) In ICS, there’s also a an OpenMAX AL API that can play back video, using the IMediaPlayerService interface.
Most of the APIs above in section 2 can either run in the calling application processes, or in the media server. Each of the Binder APIs (IAPI/BpAPI/BnAPI, such as IOMX, BpOMX, BnOMX) can proxy calls into the media server. The caller only gets an instance of the generic interface, e.g. IMediaPlayerm and does not know whether this is a proxy for calling the same interface in another process (BpOMX) or the actual implementation running within the same process (BnOMX).
Thanks to this, the whole concept of some parts of the API running within a separate process is mostly transparent when working with the APIs.
Stagefright is the library containing most of the implementation of the media framework.
Stagefright is a relatively recent development within android, replacing OpenCORE. It first appeared in Eclair, was made default player for all video (except for RTSP streaming which was still handled by OpenCORE) in Froyo, and in Gingerbread it had gained RTSP streaming support and was mature enough to be used for video recording, too, so OpenCORE was removed in Gingerbread. Since then, it has still evolved a bit.
Stagefright is mainly a generic pipeline kind of framework, similar to GStreamer, but much smaller and simpler. (It is purpose-written from scratch to fill the exact needs of Android, while OpenCORE was an already existing framework that PacketVideo provided at the time. Stagefright is a few orders of magnitude smaller than OpenCORE while still fulfilling all the needs Android has.)
Being a pipeline framework means that it consists of individual elements that form a pipeline, where each element either can produce data itself, or take input data from another element. By chaining such elements together, one can produce a pipeline that does the intended thing. E.g., for playing back video from a mp4 file on the phone, the following elements are hooked up together:
The AwesomePlayer creates the right MediaExtractor in finishSetDataSource_1, setting up the tracks in setDataSource l, and later creates the OMXCodec object for each of the tracks. The audio OMXCodec object is given as source object to AudioPlayer, which then reads data from it.
GStreamer has the same element/pipeline design, but is much more flexible supporting a number of modes of operation and a lot more elements, while Stagefright is simpler and smaller, since it only does exactly what is needed in Android.
The pipeline in Stagefright is pull-based, meaning that each element deriving from the base class MediaSource implements a method read(), which when called will block until the MediaSource element has produced one MediaBuffer which is returned. When the MediaSource subclass object is created, it is normally given an upstream MediaSource object to read from, e.g. for OMXCodec, the MediaSource is given as parameter to OMXCodec::Create().
For OMXCodec, the read() method internally reads a packet of compressed data from the source (which might e.g. be MPEG4Source within MPEG4Extractor in the case above), passes the packet to the actual OpenMAX codec for decoding, and when the decoded raw audio data is available, it is returned by the read() method.
Similarly, the MPEG4Source object will block in the read() method until it has read one packet from the source file, which is then returned to the caller. With this design, elements can be more or less arbitrarily connected, as long as one element can handle the data type that the input element produces.
For pipelines, Stagefright contains these kinds of element implementations:
In Honeycomb/Ice Cream Sandwich, Stagefright has evolved further, with a second mode of operation in addition to the plain pipeline based system. Now, the ACodec class encapsulates an OpenMAX codec with a different kind of abstraction. This class isn’t to be used as a strict pipeline element, thus, it doesn’t implement the MediaSource interface. Instead, messages are passed to ACodec (which implements AHandler, for receiving messages) with buffers to pass to the OpenMAX codec. This is more similar to the behavior model of the actual OpenMAX codecs themselves.
In addition to the elements, Stagefright contains higher level classes such as AwesomePlayer that coordinates playback of a video file, other utility classes such as color converters, wrapping of OpenMAX codecs (providing a Binder C++ interface around OpenMAX, giving it the possibility to use the codecs from a different process even if the codecs themselves run within the media server), and a number of software implementations of codecs.
|
|
FileSource, MPEG4Source and OMXCodec all implement the MediaSource interface. MPEG4Extractor, OMXCodec, AudioPlayer, MediaWriter all use the MediaSource interface for reading their input from the preceding element, of which they don’t need to know anything else than what’s in MediaSource. |
Stagefright provides wrapping around OMX core implementations. The main client api is called IOMX (frameworks/base/include/media/IOMX.h), which is a Binder interface with a proxy (BpOMX) and backend (BnOMX), allowing the caller and implementation to reside in separate processes. BnOMX is implemented by the OMX class in frameworks/base/media/libstagefright/omx/OMX.cpp, which uses the OMXMaster class in frameworks/base/media/libstagefright/omx/OMXMaster.cpp for querying and instantiating components from a number of different implementation sources. OMXMaster loads one or more plugins (OMXPluginBase, defined in frameworks/base/include/media/libstagefright/OMXPluginBase.h) that are analogous to normal OMX IL cores.
OMXMaster loads a shared library named libstagefrighthw.so, which should have a function named ZN7android15createOMXPluginEv(android::createOMXPlugin()) or createOMXPlugin (since ICS, both are tried, earlier, only the former was tried), that returns an OMXPluginBase pointer when called.
This library, libstagefrighthw.so, is vendor specific. Examples of implementations of this library, wrapping a normal OMX IL core, are available in:
All of these are very similar they simply load an OMX core from a shared library, load the OMX core function pointers from the library, and map the OMXPluginBase methods to the OMX core.
For software codecs, there’s no full proper OMX IL core in ICS, all decoders are mapped straight from the OMXPluginBase, implemented in frameworks/base/media/libstagefright/omx/SoftOMXPlugin.cpp. All software encoders (and in gingerbread, all software decoders too) are hooked up directly from the OMXCodec class, where the direct constructors of the software encoder classes are called, without any indirection via OMX like interfaces these software codecs implement the MediaSource interface directly. Due to this, these codecs cannot be accessed via the IOMX layer.
The development seems to be moving towards the OMX interfaces namely, in ICS, the software decoders have been converted to use OMX. The new ACodec class in ICS only uses codecs via the OMX interfaces.
Examples:
The OMX core acts as a registry for the available codecs. If the build configuration already contains a vendor specific OMX core, the new OMX components can either be added to this OMX core, or a new separate OMX core can be added.
Build libstagefright, containing an OMXPluginBase implementation that loads the OMX core. Examples:
If one chooses to add a second OMX core, a second libstagefrighthw (with a slightly different name) has to be created, and this also requires modifications to frameworks/base/media/libstagefright/omx/OMXMaster.cpp, in order to be able to handle two different vendor libraries at the same time. Therefore, it’s probably best to integrate new components within the current vendor OMX core if one already exists.
This is the actual wrapping of the codecs. Simple examples (which don’t use the proper real OMX API but only stagefright’s own internal API) are available in
frameworks/base/media/libstagefright/omx/SoftOMXComponent.cpp
frameworks/base/media/libstagefright/omx/SimpleSoftOMXComponent.cpp
frameworks/base/media/libstagefright/codecs/aacdec/SoftAAC.cpp
These contain all the logic any OMX component needs to have, but the external API isn’t the proper public OMX version, but only stagefright-internal ones. Real implementations with the proper public OMX API are available in e.g.:
Register the new OMX components in frameworks/base/media/libstagefright/OMXCodec.cpp. Simply add the component names and the mime types it handles in the kDecoderInfo/kEncoderInfo tables. (OMXCodec in stagefright doesn’t query for which components implement certain roles, but blindly checks for all the components listed to support a certain mime type, and uses the first one that actually exists.)
In this section, we’ll explore the inter-workings of the telephony stack. We’ll start with the standard Android Phone app, and trace the execution of placing a call all the way down to RIL daemon.
Android Framework provides the Telephony Manager class in android.telephony package. The Telephony Manager allows you to monitor the state of the mobile network connection. However, Telephony Manager does not allow you to place or manage any calls. Only the Phone app can initiate and answer phone calls.
|
|
Some of this content comes from the actual AOSP source code, licensed under Apache 2 License. |
Phone app does come with some UI components, such as the dialer, but the most significant part is its handling of CALL and CALL_PRIVILEGED intents to do the actual dialing of a number.
OutgoingCallBroadcaster activity receives CALL and CALL_PRIVILEGED Intents, and broadcasts the ACTION_NEW_OUTGOING_CALL intent which allows other applications to monitor, redirect, or prevent the outgoing call. After the other applications have had a chance to see the ACTION_NEW_OUTGOING_CALL intent, it finally reaches the OutgoingCallReceiver, which passes the (possibly modified) intent on to the SipCallOptionHandler, which will ultimately start the call using the CallController.placeCall() API.
CallController handler is the phone app module in charge of call control. This is a singleton object which acts as the interface to the telephony layer (and other parts of the Android framework) for all user-initiated telephony functionality, like making outgoing calls.
This functionality includes things like:
The single CallController instance stays around forever; it’s not tied to the lifecycle of any particular Activity (like the InCallScreen). There’s also no implementation of onscreen UI here (that’s all in InCallScreen).
Note that this class does not handle asynchronous events from the telephony layer, like reacting to an incoming call; see CallNotifier for that. This class purely handles actions initiated by the user, like outgoing calls.
placeCall(Intent intent) initiates an outgoing call.
Here’s the most typical outgoing call sequence:
OutgoingCallBroadcaster receives a CALL intent and sends the NEW_OUTGOING_CALL broadcast.
The broadcast finally reaches OutgoingCallReceiver, which stashes away a copy of the original CALL intent and launches SipCallOptionHandler.
SipCallOptionHandler decides whether this is a PSTN or SIP call (and in some cases brings up a dialog to let the user choose), and ultimately calls CallController.placeCall() (from the setResultAndFinish() method) with the stashed-away intent from step (2) as the "intent" parameter.
Here in CallController.placeCall() we read the phone number or SIP address out of the intent and actually initiate the call, and simultaneously launch the InCallScreen to display the in-call UI.
We handle various errors by directing the InCallScreen to display error messages or dialogs (via the InCallUiState "pending call status code" flag), and in some cases we also sometimes continue working in the background to resolve the problem (like in the case of an emergency call while in airplane mode). Any time that some onscreen indication to the user needs to change, we update the "status dialog" info in the inCallUiState and (re)launch the InCallScreen to make sure it’s visible.
PhoneUtils.placeCall(Context context, Phone phone, String number, Uri contactRef, boolean isEmergencyCall, Uri gatewayUri) dials the number using the phone passed in.
CallManager, defined in PhoneApp class.
CallManager class provides an abstract layer for PhoneApp to access and control calls. It implements Phone interface.
CallManager provides call and connection control as well as channel capability.
There are three categories of APIs CallManager provided
Call control and operation, such as dial() and hangup()
Channel capabilities, such as CanConference()
Register notification
public Connection dial(Phone phone, String dialString) in CallManager initiate a new voice connection. This happens asynchronously, so you cannot assume the audio path is connected (or a call index has been assigned) until PhoneStateChanged notification has occurred.
dial() uses PhoneProxy to implement Phone interface to make the call.
Phone interface specifies the capabilities of the underlying phone system, weather GSM, CDMA, or SIP. We’ll assume GMS from here on.
GSMPhone extends PhoneBase which in turn implements the Phone interface.
It uses GSMCallTracker to dial via CommandsInterface. This interface is implemented by RIL class.
RIL in turn uses RILRequest to send the requests to rild daemon.
|
|
You can use adb logcat -b radio to see the radio-specific log messages. |
RIL class is the implementation of the CommandsInterface that GSMPhone uses to dial out. This Java implementation writes out the commands to RIL daemon, the native code.
RIL uses RILSender and RILReceiver to send and receive messages from rild - the RIL Daemon. Unlike most of the other Android system services, RIL uses sockets for this inter-process communication, and not the Binder.
The RIL consists of two primary components:
|
|
Some of the following documentation comes from once available Android Platform Development Kit: Radio Layer Interface documentation. This documentation is no longer readily available from Google (deemed outdated) but is licensed under Apache 2.0 license. We’ve updated the references as originally provided. |
Android initializes the telephony stack and the Vendor RIL at startup as described in the sequence below:
RIL daemon reads rild.lib path and rild.libargs system properties to determine the Vendor RIL library to use and any initialization arguments to provide to the Vendor RIL.
RIL daemon loads the Vendor RIL library and calls RIL_Init to initialize the RIL and obtain a reference to RIL functions.
RIL daemon calls RIL_register on the Android telephony stack, providing a reference to the Vendor RIL functions
Details of this implementation are available in AOSP/hardware/ril/rild/rild.c.
There are two forms of communication that the RIL handles:
The following snippet illustrates the interface for solicited commands:
There are over sixty solicited commands grouped by the following families:
The following snippet illustrates the interface for unsolicited commands:
There are over ten unsolicited commands grouped by the following families:
To implement a radio-specific RIL, create a shared library that implements a set of functions required by Android to process radio requests. The required functions are defined in the RIL header (AOSP/hardware/ril/include/telephony/ril.h).
The Android radio interface is radio-agnostic and the Vendor RIL can use any protocol to communicate with the radio. Android provides a reference Vendor RIL, using the Hayes AT command set, that you can use as a quick start for telephony testing and a guide for commercial vendor RILs. The source code for the reference RIL is found at AOSP/hardware/ril/reference-ril.
Compile your Vendor RIL as a shared library using the convention libril-<companyname>-<RIL version>.so, for example, libril-acme-124.so, where:
For reference implementation of RIL, see AOSP/hardware/ril/libril.
It performs the measure and layout operation on its (invalidated) view hierarchy
It prepares to draw by asking its surface (which is just a client to the Surface Flinger) to dequeueBuffer,
The Surface Flinger asks Gralloc HAL for a buffer
The Gralloc HAL asks the kernel’s memory allocator to give it a block of physical memory (usually via PMEM, UMP, DMABUF, ION)
The Gralloc HAL provides a handle to this memory buffer back go Surface Flinger
The Surface Flinger sends this reference to the buffer as a file descriptor back to app via binder
The app gets the FD to the buffer
It does not explicitly try to map it or understand the memory - because this opaque memory blob is unique to the Gralloc implementation and the OpenGL drivers
It aggregates and optimizes all of its drawing commands into a display list
Issues the commands from the display list via the OpenGL API → driver into the buffer
Asks its surface (i.e. the Surface Flinger) to queueBuffer, which actually signals the Surface Flinger that the buffer is ready and that it should be posted to the screen at the next VSYNC
Figures out which buffers are visible on the screen as well as their relative z-order (with the help of WindowManagerService)
Composites (fuses) all the window buffers together using:
Sends the final image to the display via
See Exposing the Android Camera Stack from 2012 Android Builders Summit by Balwinder Kaur and Joe Rickson from Aptina Imaging, Inc.
|
|
This service is deployed as an application! |
Name at least five application framework services?
What is the most typical communication channel for a client to communicate with its service?
What’s the difference between managers and services?
How do we get the list of all application framework services?
What is the purpose of the servicemanager process?
How do clients/services get access to the servicemanager process?
What role does JNI play in most application framework services?
What is the life-cycle of most application framework services?
What is the relationship between application framework services and daemons?
How do most services communicate with their daemons?
What is the purpose of installd and who uses it and when?
What is the purpose of wpa_supplicant and who uses it?
Unlike the Vibrator (or Power) service, the Alarm service does not have what?
How do services (like Location service) provide call-backs to their clients?
What is AudioFlinger and what is its purpose?
What is the role of Audio Policy Service?
What is Stagefright and what is its purpose?
How do hardware manufactures take advantage of Stagefright?
How does the telephony stack differ from most other services?
What is SurfaceFlinger and what is its purpose?
How does NFC Service differ from most other services?
|
|
The following steps assume building Android ICS 4.0.3/4 for ARMv7 on Ubuntu 10.04 x86_64 and that the Android sources are available under ~/ics/ directory (for the current user). |
|
|
The complete code (which we’ll develop in this section) for Marakana Alpha is available at https://github.com/marakana/alpha The code for Marakana Alpha SDK add-on is available https://github.com/marakana/alpha_sdk_addon |
While we could overlay our device’s custom components over the existing AOSP source tree, that makes it harder to deal with future OS upgrades. Instead, we will create a self-contained directory structure to host our device.
Create our vendor (e.g. mararkana) directory
$ mkdir device/marakana/
Now create our device (e.g. alpha) sub-directory:
$ mkdir device/marakana/alpha
We now want to add our device to the Android’s lunch list
|
|
Remember that $ source build/envsetup.sh registers lunch combos that we can later build |
Create vendorsetup.sh file for our device (the name of this file is fixed):
add_lunch_combo full_marakana_alpha-eng
Re-build the lunch list:
$ source build/envsetup.sh including device/marakana/alpha/vendorsetup.sh including device/moto/stingray/vendorsetup.sh including device/moto/wingray/vendorsetup.sh including device/samsung/crespo4g/vendorsetup.sh including device/samsung/crespo/vendorsetup.sh including device/samsung/maguro/vendorsetup.sh including device/samsung/torospr/vendorsetup.sh including device/samsung/toro/vendorsetup.sh including device/samsung/tuna/vendorsetup.sh including device/ti/panda/vendorsetup.sh including sdk/bash_completion/adb.bash
Finally, we can check to see that our device now appear in the lunch menu:
$ lunch
You're building on Linux
Lunch menu... pick a combo:
1. full-eng
2. full_x86-eng
3. vbox_x86-eng
4. full_marakana_alpha-eng
5. full_stingray-userdebug
6. full_wingray-userdebug
7. full_crespo4g-userdebug
8. full_crespo-userdebug
9. full_maguro-userdebug
10. full_torospr-userdebug
11. full_toro-userdebug
12. full_tuna-userdebug
13. full_panda-eng
Which would you like? [full-eng]
$ lunch full_marakana_alpha-eng build/core/product_config.mk:203: *** No matches for product "full_marakana_alpha". Stop. ** Don't have a product spec for: 'full_marakana_alpha' ** Do you have the right repo manifest?
We now need to add basic support for building our device.
Start by creating a our own AndroidProducts.mk file, which simply defines the actual makefiles to be used when building our device:
PRODUCT_MAKEFILES := $(LOCAL_DIR)/full_alpha.mk
|
|
The only purpose of AndroidProducts.mk file (whose name is fixed) is to set PRODUCT_MAKEFILES to a list of product makefiles to expose to the build system. The only external variable it can use is LOCAL_DIR, whose value will be automatically set to the directory containing this file. |
Create the main build-file for our device (we call it full_alpha following the example from device/samsung/crespo/full_crespo.mk):
# Defines a list of languages to be supported by our device $(call inherit-product, $(SRC_TARGET_DIR)/product/languages_small.mk) # Defines the rules for building the base Android platform, # but itself is not specialized for any particular device $(call inherit-product, $(SRC_TARGET_DIR)/product/generic.mk) # Discard inherited values and use our own instead. PRODUCT_NAME := full_marakana_alpha PRODUCT_DEVICE := alpha PRODUCT_MODEL := Full Marakana Alpha Image for Emulator
|
|
A note inheriting from an existing product
Our device inherits from the generic product: $(call inherit-product, $(SRC_TARGET_DIR)/product/generic.mk) which resolves to build/target/product/generic.mk file. This generic product (file) in turn inherits from:
The combination of these files set up compilation rules that define what gets included in the final product. |
Next, we’ll import some boiler-plate make/support files from the "generic" board - since our device will run on the emulator:
$ cp build/target/board/generic/BoardConfig.mk device/marakana/alpha/. $ cp build/target/board/generic/AndroidBoard.mk device/marakana/alpha/. $ cp build/target/board/generic/device.mk device/marakana/alpha/. $ cp build/target/board/generic/system.prop device/marakana/alpha/.
# config.mk # # Product-specific compile-time definitions. # # The generic product target doesn't have any hardware-specific pieces. TARGET_NO_BOOTLOADER := true TARGET_NO_KERNEL := true # Note: we build the platform images for ARMv7-A _without_ NEON. # # Technically, the emulator supports ARMv7-A _and_ NEON instructions, but # emulated NEON code paths typically ends up 2x slower than the normal C code # it is supposed to replace (unlike on real devices where it is 2x to 3x # faster). # # What this means is that the platform image will not use NEON code paths # that are slower to emulate. On the other hand, it is possible to emulate # application code generated with the NDK that uses NEON in the emulator. # TARGET_ARCH_VARIANT := armv7-a TARGET_CPU_ABI := armeabi-v7a TARGET_CPU_ABI2 := armeabi HAVE_HTC_AUDIO_DRIVER := true BOARD_USES_GENERIC_AUDIO := true # no hardware camera USE_CAMERA_STUB := true # Set /system/bin/sh to ash, not mksh, to make sure we can switch back. TARGET_SHELL := ash # Enable dex-preoptimization to speed up the first boot sequence # of an SDK AVD. Note that this operation only works on Linux for now ifeq ($(HOST_OS),linux) WITH_DEXPREOPT := true endif # Build OpenGLES emulation guest and host libraries BUILD_EMULATOR_OPENGL := true # Build and enable the OpenGL ES View renderer. When running on the emulator, # the GLES renderer disables itself if host GL acceleration isn't available. USE_OPENGL_RENDERER := true
|
|
A note about CPU Architecture
Because we copied build/target/board/generic/BoardConfig.mk we inherited the following: TARGET_ARCH_VARIANT := armv7-a TARGET_CPU_ABI := armeabi-v7a TARGET_CPU_ABI2 := armeabi Alternatively, we could have built our image for x86, either by copying build/target/board/generic_x86/BoardConfig.mk or by changing our own device/marakana/alpha/BoardConfig.mk to say the following: TARGET_ARCH := x86 TARGET_ARCH_VARIANT := x86-atom TARGET_CPU_ABI := x86 |
Before we compile our device, we should generate our own platform signing keys [Platform_Keys], otherwise our device will not pass CTS.
Define our subject/issuer info:
$ SIGNER="/C=US/ST=California/L=San Francisco/O=Marakana Inc./OU=Android/CN=Android Platform Signer/emailAddress=android@marakana.com"
Remove the existing keys (it does not hurt to back them up first!):
$ rm build/target/product/security/*.p*
Generate the platform key:
$ echo | development/tools/make_key build/target/product/security/platform "$SIGNER" creating build/target/product/security/platform.pk8 with no password Generating RSA private key, 2048 bit long modulus ....................+++ ..........................................................+++ e is 3 (0x3)
Generate the shared key:
$ echo | development/tools/make_key build/target/product/security/shared "$SIGNER" creating build/target/product/security/shared.pk8 with no password Generating RSA private key, 2048 bit long modulus ..................................................................................................+++ ............+++ e is 3 (0x3)
Generate the media key:
$ echo | development/tools/make_key build/target/product/security/media "$SIGNER" creating build/target/product/security/media.pk8 with no password Generating RSA private key, 2048 bit long modulus ...................+++ ....................+++ e is 3 (0x3)
Generate the testkey key:
$ echo | development/tools/make_key build/target/product/security/testkey "$SIGNER" creating build/target/product/security/testkey.pk8 with no password Generating RSA private key, 2048 bit long modulus ....................+++ ................................................+++ e is 3 (0x3)
Verify that our keys have been created:
$ ls -1 build/target/product/security/*.p* build/target/product/security/media.pk8 build/target/product/security/media.x509.pem build/target/product/security/platform.pk8 build/target/product/security/platform.x509.pem build/target/product/security/shared.pk8 build/target/product/security/shared.x509.pem build/target/product/security/testkey.pk8 build/target/product/security/testkey.x509.pem
Check that our specific subject/issuer has been used:
$ openssl x509 -noout -subject -issuer -in build/target/product/security/platform.x509.pem subject= /C=US/ST=California/L=San Francisco/O=Marakana Inc./OU=Android/CN=Android Platform Signer/emailAddress=android@marakana.com issuer= /C=US/ST=California/L=San Francisco/O=Marakana Inc./OU=Android/CN=Android Platform Signer/emailAddress=android@marakana.com
|
|
The build/target/product/security/.pk8 files are the private keys (.x509.pem are the certificates), and we need to make sure to keep them safe and secure - especially since we did not encrypt them! |
We are now ready to try out our "custom" device - though there is nothing truly custom yet, except for the PRODUCT_* settings and our own product keys.
For good measure, re-register our device:
$ source build/envsetup.sh including device/marakana/alpha/vendorsetup.sh …
Now we can lunch of our device:
$ lunch full_marakana_alpha-eng ============================================ PLATFORM_VERSION_CODENAME=REL PLATFORM_VERSION=4.0.3 TARGET_PRODUCT=full_marakana_alpha TARGET_BUILD_VARIANT=eng TARGET_BUILD_TYPE=release TARGET_BUILD_APPS= TARGET_ARCH=arm TARGET_ARCH_VARIANT=armv7-a HOST_ARCH=x86 HOST_OS=linux HOST_BUILD_TYPE=release BUILD_ID=IML74K ============================================
We can now compile our device:
$ export USE_CCACHE=1 $ make -j10 … Installed file list: out/target/product/alpha/installed-files.txt Target system fs image: out/target/product/alpha/obj/PACKAGING/systemimage_intermediates/system.img Install system fs image: out/target/product/alpha/system.img
Finally, we can run it
$ out/host/linux-x86/bin/emulator &
|
|
Replace linux with darwin for Mac OS X |
And we should see
Our device would work fine with the provided QEMU-based (i.e. emulator-specific) kernel:
In this section, we will configure a custom kernel ([Android_Building_Linux_Kernel]) so that we can:
The steps outlined here are similar to [Android_Building_Linux_Kernel_for_Emulator]:
Create a directory to host our kernel sources:
$ mkdir ~/kernel/ $ cd ~/kernel/
Clone the Goldfish kernel sources:
$ git clone https://android.googlesource.com/kernel/goldfish.git $ cd goldfish/
Check out android-goldfish-2.6.29 branch of the kernel we wish to build:
$ git checkout -t remotes/origin/android-goldfish-2.6.29
Create the default kernel configuration file (.config):
$ make goldfish_armv7_defconfig ARCH=arm
|
|
Depending on TARGET_ARCH_VARIANT that we specified in device/marakana/alpha/BoardConfig.mk: to compile for ARMv5, we would run make goldfish_defconfig ARCH=arm instead. to compile for x86, we would run make goldfish_defconfig ARCH=x86 instead. |
Change the Hardware name from "Goldfish" to "Marakana Alpha Board":
… MACHINE_START(GOLDFISH, "Marakana Alpha Board") …
|
|
A note about the hardware (board) name
As we already know, when the Android kernel loads, it runs init, which configures itself from init.rc and init.hardware_.rc configuration files. The hardware name is extracted from /proc/cpuinfo: $ adb shell cat /proc/cpuinfo |grep Hardware Hardware : Goldfish Basically, system/core/init/init.c:main() uses system/core/init/util.c:get_hardware_name(…) to parse /proc/cpuinfo and extract the hardware name. This name is converted to lower-case and all the space characters are trimmed. Android’s ueventd process (started by init) also configures itself in a similar way, by loading ueventd.rc and ueventd.hardware.rc configuration files. Since in our case we changed the name of our hardware to "Marakana Alpha Board", we will also need to create the corresponding: init.marakanaalphaboard.rc and ueventd.marakanaalphaboard.rc files. |
Configure the kernel
$ make menuconfig ARCH=arm
|
|
For x86, run $ make menuconfig ARCH=x86 instead. |
Set General setup → Local version to -marakana-alpha-release
Select Enable loadable module support and within it also select Module unloading and within that Forced module unloading
Customize the rest as desired
Compile the kernel
$ make -j10 ARCH=arm CROSS_COMPILE=~/ics/prebuilt/linux-x86/toolchain/arm-eabi-4.4.3/bin/arm-eabi-
|
|
For x86, run make -j10 ARCH=x86 CROSS_COMPILE=~/ics/prebuilt/linux-x86/toolchain/i686-unknown-linux-gnu-4.2.1/bin/i686-unknown-linux-gnu- instead. |
|
|
We ran into issues (clobbered PIC register) last time we tried compiling for x86 (January 2012) with alternative i686 toolchains. |
|
|
Remember that ~/ics/ is the root of the Android Open Source Project (AOSP) source tree |
|
|
AOSP comes with a convenient script to compile the Linux kernel for QEMU-based emulator: ~/ics/external/qemu/distrib/build-kernel.sh, but this picked the wrong toolchain for x86. |
Copy the compiled kernel to our device’s alpha/ directory:
$ cp arch/arm/boot/zImage ~/ics/device/marakana/alpha/kernel
|
|
For x86, copy arch/x86/boot/bzImage instead. |
|
|
In case you were not able to compile the kernel, you could also download a pre-built one from https://github.com/marakana/alpha/raw/master/kernel |
Now we can go back to the AOSP source
$ cd ~/ics/
Enable our custom kernel in BoardConfig.mk (double-negation, nice!):
… TARGET_NO_KERNEL := false …
|
|
This will cause the build system to rebuild the boot.img file, but because we are still working with the emulator, we’ll have to explicitly reference our kernel on startup. |
Now we are ready to create our own init and ueventd configuration files by copying the ones from Goldfish:
$ cp system/core/rootdir/etc/init.goldfish.rc device/marakana/alpha/init.marakanaalphaboard.rc $ cp system/core/rootdir/etc/ueventd.goldfish.rc device/marakana/alpha/ueventd.marakanaalphaboard.rc
|
|
Remember, marakanaalphaboard is our device’s new hardware name (lowercased and stripped of spaces). |
Create a common.mk makefile for our common components, where we’ll configure our kernel and copy the init and ueventd configuration files:
# Since this file can also be referenced by alpha-sdk_addon # we cannot assume LOCAL_PATH points to the directory where # this file is located. Instead, we create another variable # to capture this directory. MY_PATH := $(LOCAL_PATH)/../alpha # Include all makefiles in sub-directories (one level deep) include $(call all-subdir-makefiles) # Enable our custom kernel LOCAL_KERNEL := $(MY_PATH)/kernel PRODUCT_COPY_FILES += $(LOCAL_KERNEL):kernel # Copy our init and ueventd configuration files to the root # file system (ramdisk.img -> boot.img) PRODUCT_COPY_FILES += $(MY_PATH)/init.marakanaalphaboard.rc:root/init.marakanaalphaboard.rc PRODUCT_COPY_FILES += $(MY_PATH)/ueventd.marakanaalphaboard.rc:root/ueventd.marakanaalphaboard.rc
Now, we need to include our common common.mk file in our device’s main makefile (full_alpha.mk):
… # Include the common definitions and packages include $(LOCAL_PATH)/common.mk
Re-build our ROM, which, in this case just creates a new out/target/product/alpha/boot.img (that we won’t actually use):
$ make -j10 …
Restart the emulator (with our new kernel)
$ out/host/linux-x86/bin/emulator -kernel out/target/product/alpha/kernel &
|
|
We have to run our emulator with -kernel, because otherwise it would ignore the kernel in our newly built boot.img and instead use the default one (prebuilt/android-arm/kernel/kernel-qemu-armv7). This applies just to the emulator. |
Test
$ adb shell cat /proc/version Linux version 2.6.29-marakana-alpha-release-g46b05b2 (sasa@thermal) (gcc version 4.4.3 (GCC) ) #3 Tue Jan 3 13:25:44 PST 2012
|
|
The path to adb was added to our PATH when we $ source build/envsetup.sh (on a Linux host, it comes from out/host/linux-x86/bin/adb) |
We could also take a look at the updated About screen:
We start by creating a home for our header files:
$ mkdir device/marakana/alpha/include/
Next, we can create a simple header file (mrknlog.h) for our library:
#ifndef _MRKN_LOG_H_ #define _MRKN_LOG_H_ #ifdef __cplusplus extern "C" { #endif extern int mrkn_flush_log(); extern int mrkn_get_total_log_size(); extern int mrkn_get_used_log_size(); #ifdef __cplusplus } /* End of the 'extern "C"' block */ #endif #endif /* End of the _MRKN_LOG_H_ block */
Since we are already creating our global headers, we might as well define some convenience utility macros that we’ll use later (for logging):
#ifndef _MRKN_UTILS_H #define _MRKN_UTILS_H #include <android/log.h> #ifdef __cplusplus extern "C" { #endif #ifndef LOG_TAG #define LOG_TAG NULL #endif #ifndef LOGE #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #endif #ifndef LOGW #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) #endif #ifndef LOGI #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #endif #ifndef LOGD #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #endif #ifdef __cplusplus } #endif #endif /* _MRKN_UTILS_H */
|
|
The macros in this file are redundant to what Android already defines in <cutils/log.h>, but the macros there were renamed in the 4.1 (Jelly Bean) release (e.g. LOGE(…) → ALOGE(…)), which is why we create our own. |
Next, we create a home for our shared libraries:
$ mkdir device/marakana/alpha/lib/
Since our actual libraries will be in their own sub-directories, we need to include them in our build system, via a simple makefile:
include $(call all-subdir-makefiles)
Now we can create an actual directory for our libmrknlog library:
$ mkdir device/marakana/alpha/lib/libmrknlog
Next, we implement our shared library (libmrknlog.c) - using ioctl() to talk to the kernel driver:
#define LOG_FILE "/dev/log/main" #define LOG_TAG "MrknLog" #include <mrknlog.h> #include <mrknutils.h> #include <cutils/logger.h> #include <string.h> #include <fcntl.h> #include <errno.h> #include <sys/ioctl.h> #include <sys/stat.h> #include <sys/types.h> static int ioctl_log(int mode, int request) { int logfd = open(LOG_FILE, mode); if (logfd < 0) { LOGE("Failed to open %s: %s", LOG_FILE, strerror(errno)); return -1; } else { int ret = ioctl(logfd, request); close(logfd); return ret; } } extern int mrkn_flush_log() { return ioctl_log(O_WRONLY, LOGGER_FLUSH_LOG); } extern int mrkn_get_total_log_size() { return ioctl_log(O_RDONLY, LOGGER_GET_LOG_BUF_SIZE); } extern int mrkn_get_used_log_size() { return ioctl_log(O_RDONLY, LOGGER_GET_LOG_LEN); }
We are now ready for the makefile (Android.mk):
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := libmrknlog.c LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../include/ LOCAL_SHARED_LIBRARIES := libcutils LOCAL_MODULE := libmrknlog include $(BUILD_SHARED_LIBRARY)
|
|
Here, we use: LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../include/ in order to include our headers and LOCAL_SHARED_LIBRARIES := libcutils in order to link to __android_log_print (used by the LOGE macro). |
|
|
In previous versions of Android (pre 4.0), we would’ve also needed to add LOCAL_PRELINK_MODULE := false in order to prevent pre-linking of our library. Pre-linked libraries were registered to be loaded at fixed memory addresses in order to speed up linking and allow for faster booting (at the expense of ASLR). If we did not disable it, we would’ve had to register our library build/core/prelink-linux-arm.map (now deprecated). |
Optionally, we can compile our library to test that it builds:
$ make libmrknlog … target thumb C: libmrknlog <= device/marakana/alpha/lib/libmrknlog/libmrknlog.c target SharedLib: libmrknlog (out/target/product/alpha/obj/SHARED_LIBRARIES/libmrknlog_intermediates/LINKED/libmrknlog.so) target Symbolic: libmrknlog (out/target/product/alpha/symbols/system/lib/libmrknlog.so) target Strip: libmrknlog (out/target/product/alpha/obj/lib/libmrknlog.so) Install: out/target/product/alpha/system/lib/libmrknlog.so
|
|
A note about compiling individual modules
Android’s build system allows us to compile individual modules via a simple $ make <module-name> where <module-name> is defined by LOCAL_MODULE (or LOCAL_PACKAGE_NAME for apps) in the module’s Android.mk file. To clean just this one module, we can do $ make clean-<module-name>. This works, but it still takes a long time (about 21.2s on 3.4GHz i7 with SSD) because the build system has to scan the entire source tree to find our module. Alternatively, we can use the mm function right from the module’s directory (takes about 1.6s): $ godir libmrknlog device/marakana/alpha/lib/libmrknlog$ mm … make: Entering directory ++/flash/ics-4.0.3' target thumb C: libmrknlog <= device/marakana/alpha/lib/libmrknlog/libmrknlog.c target SharedLib: libmrknlog (out/target/product/alpha/obj/SHARED_LIBRARIES/libmrknlog_intermediates/LINKED/libmrknlog.so) target Symbolic: libmrknlog (out/target/product/alpha/symbols/system/lib/libmrknlog.so) target Strip: libmrknlog (out/target/product/alpha/obj/lib/libmrknlog.so) Install: out/target/product/alpha/system/lib/libmrknlog.so make: Leaving directory ++/flash/ics-4.0.3' device/marakana/alpha/lib/libmrknlog$ croot $ The function godir builds a filelist index of all of the source-files, searches for the file that matches the argument name in that list, and then jumps to the directory containing that file. In this case, it’s equivalent to cd device/marakana/alpha/lib/libmrknlog command. The function croot goes to the root of the AOSP source tree. In this case, it’s equivalent to cd -. Another way to compile just a particular module (by its directory path) is to use the mmm function from the root of the source tree: $ mmm device/marakana/alpha/lib/libmrknlog … Bash functions godir, croot, mm, and mmm are defined by build/envsetup.sh, which we sourced into our shell. |
Since our library’s Android.mk states LOCAL_MODULE_TAGS := optional, we need to register it with Alpha’s PRODUCT_PACKAGES, otherwise it will not be included in the final ROM:
…
PRODUCT_PACKAGES += libmrknlogBefore we can create our executable to test our library, let’s create a directory for all binaries:
$ mkdir device/marakana/alpha/bin
Like with our libraries, we need to "recursively" include makefiles in sub-directories of …/bin/, which is where our executables will live:
include $(call all-subdir-makefiles)
Now we can create a directory for our mrknlog executable:
$ mkdir device/marakana/alpha/bin/mrknlog
Next we provide the implementation (mrknlog.c), which will use our shared library (libmrknlog):
#include <stdio.h> #include <string.h> #include <errno.h> #include <mrknlog.h> int main (int argc, char* argv[]) { int usedSize = mrkn_get_used_log_size(); int totalSize = mrkn_get_total_log_size(); if (totalSize >= 0 && usedSize >= 0) { if (mrkn_flush_log() == 0) { printf("Flushed log. Previously it was consuming %d of %d bytes\n", usedSize, totalSize); return 0; } else { fprintf(stderr, "Failed to flush log: %s", strerror(errno)); } } else { fprintf(stderr, "Failed to get log size: %s", strerror(errno)); } return -1; }
As with everything else, we need a makefile (Android.mk) to build our executable:
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := mrknlog.c LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../include/ LOCAL_SHARED_LIBRARIES := libmrknlog LOCAL_MODULE := mrknlog include $(BUILD_EXECUTABLE)
|
|
Here, we need to tell the compiler where to find our library’s header file: LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../include/ and tell the linker to link against our library: LOCAL_SHARED_LIBRARIES := libmrknlog Finally, unlike last time when we include $(BUILD_SHARED_LIBRARY), this time we: include $(BUILD_EXECUTABLE) which will produce system/bin/mrknlog |
We can compile our executable to test that it builds:
$ rm filelist $ godir mrknlog.c [1] ./device/marakana/alpha/bin/mrknlog [2] ./device/marakana/alpha/lib/libmrknlog Select one: 1 device/marakana/alpha/bin/mrknlog$ mm … target thumb C: mrknlog <= device/marakana/alpha/bin/mrknlog/mrknlog.c target Executable: mrknlog (out/target/product/alpha/obj/EXECUTABLES/mrknlog_intermediates/LINKED/mrknlog) target Symbolic: mrknlog (out/target/product/alpha/symbols/system/bin/mrknlog) target Strip: mrknlog (out/target/product/alpha/obj/EXECUTABLES/mrknlog_intermediates/mrknlog) Install: out/target/product/alpha/system/bin/mrknlog … device/marakana/alpha/bin/mrknlog$ croot $
Just like with our library, since our executable’s Android.mk states LOCAL_MODULE_TAGS := optional, we need to register it with Alpha’s PRODUCT_PACKAGES, or otherwise it will not be included in the final ROM:
…
PRODUCT_PACKAGES += mrknlogNow we can rebuild our entire device:
$ make -j10 … Install system fs image: out/target/product/alpha/system.img
Finally, we are ready to test our library/executable:
# (re)start the emulator $ out/host/linux-x86/bin/emulator -kernel out/target/product/alpha/kernel & # wait for the emulator to finish # check out our library $ adb shell ls -l /system/lib/libmrknlog.so -rw-r--r-- root root 5468 2012-01-04 10:02 libmrknlog.so # check out our utility $ adb shell ls -l /system/bin/mrknlog -rwxr-xr-x root shell 5612 2012-01-04 10:20 mrknlog # check if our utility is doing what it is supposed to $ adb logcat -g /dev/log/main: ring buffer is 64Kb (54Kb consumed), max entry is 4096b, max payload is 4076b $ adb shell /system/bin/mrknlog Flushed log. Previously it was consuming 55981 of 65536 bytes $ adb logcat -g /dev/log/main: ring buffer is 64Kb (0Kb consumed), max entry is 4096b, max payload is 4076b # check again $ adb shell /system/bin/mrknlog Flushed log. Previously it was consuming 0 of 65536 bytes # good :-)
As before, we need to create a home for our daemon source:
$ mkdir device/marakana/alpha/bin/mrknlogd
Next, we create or daemon source (mrknlogd.c), by utilizing our libmrknlog library:
#define LOG_TAG "MRKN Log Daemon" #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <mrknlog.h> #include <mrknutils.h> int main (int argc, char* argv[]) { if (argc != 2) { fprintf(stderr, "Usage: %s <flush-frequency-in-seconds>\n", argv[0]); exit(2); } else { int frequency = atoi(argv[1]); int totalSize = mrkn_get_total_log_size(); int usedSize; int count = 1; while(1) { usedSize = mrkn_get_used_log_size(); if (mrkn_flush_log() == 0) { LOGI("Flushed log (%d, %d of %d bytes). Waiting %d seconds before the next flush.", count, usedSize, totalSize, frequency); count++; } else { LOGE("Failed to flush log. Waiting %d seconds before the next attempt", frequency); } sleep(frequency); } } }
|
|
This program is designed to run as a "daemon" simply by running infinitely in a while(1) {…} block. |
|
|
We can use LOGI(), LOGE(), and other such macros, which simplify logging: #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) |
Next, we create our makefile (Android.mk), which defines links from our source to our library:
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := mrknlogd.c LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../include/ LOCAL_SHARED_LIBRARIES := libmrknlog libcutils LOCAL_MODULE := mrknlogd include $(BUILD_EXECUTABLE)
Now we can compile our daemon to test that it builds:
$ make -j10 mrknlogd … target Executable: mrknlogd (out/target/product/alpha/obj/EXECUTABLES/mrknlogd_intermediates/LINKED/mrknlogd) target Non-prelinked: mrknlogd (out/target/product/alpha/symbols/system/bin/mrknlogd) target Strip: mrknlogd (out/target/product/alpha/obj/EXECUTABLES/mrknlogd_intermediates/mrknlogd) Install: out/target/product/alpha/system/bin/mrknlogd
Just like with our previous executable/library, we need to register our daemon with Alpha’s PRODUCT_PACKAGES, or otherwise it will not be included in the final ROM:
…
PRODUCT_PACKAGES += mrknlogdBefore we can rebuild our ROM, we need to configure init to start our mrknlogd daemon as a service, and to do that, we need to register this in our init.marakanaalphaboard.rc file:
…
on boot
…
start mrknlogd
…
# Marakana's custom log-flushing daemon
service mrknlogd /system/bin/mrknlogd 60
user system
group log
oneshot
|
|
A note about security
In order to get the size of the log buffer /dev/log/main, we need to be able to read from this "file", and for that, we need to be either root or belong to group log (we chose the latter): $ adb shell ls -l /dev/log/main crw-rw--w- root log 10, 57 2012-01-05 00:02 main |
Now we can rebuild our entire device:
$ make -j10 … build/core/Makefile:25: warning: overriding commands for target ++out/target/product/alpha/root/init.goldfish.rc' system/core/rootdir/Android.mk:58: warning: ignoring old commands for target ++out/target/product/alpha/root/init.goldfish.rc' … Install system fs image: out/target/product/alpha/system.img Installed file list: out/target/product/alpha/installed-files.txt
|
|
There warnings are (unfortunately) expected because our rule to copy init.goldfish.rc (implemented by build/core/Makefile) conflicts with what Android wants to do already (in system/core/rootdir/Android.mk). |
Finally, we are ready to test our daemon:
# (re)start the emulator $ out/host/linux-x86/bin/emulator -kernel out/target/product/alpha/kernel & # wait for the emulator to finish # check out our daemon $ adb shell ls -l /system/bin/mrknlogd -rwxr-xr-x root shell 5636 2012-01-04 20:52 mrknlogd # check that it runs $ adb shell ps | grep mrknlogd system 44 1 772 300 c0051854 40012c74 S /system/bin/mrknlogd $ adb logcat | grep MRKN I/MRKN Log Daemon( 37): Flushed log (1, 60 of 65536 bytes). Waiting 60 seconds before the next flush. I/MRKN Log Daemon( 37): Flushed log (2, 34406 of 65536 bytes). Waiting 60 seconds before the next flush. I/MRKN Log Daemon( 37): Flushed log (3, 232 of 65536 bytes). Waiting 60 seconds before the next flush. ^C $ adb logcat -g /dev/log/main: ring buffer is 64Kb (0Kb consumed), max entry is 4096b, max payload is 4076b # good :-)
Since our Java/JNI library will primarily service the Application Framework layer, let’s create a home for our framework components:
$ mkdir device/marakana/alpha/framework
Like with our libraries and executables, we need to "recursively" include makefiles in sub-directories of …/framework/, which is where our Java/JNI library will live:
include $(call all-subdir-makefiles)
Now, let’s create the home for our Java/JNI library and two sub-directories for its Java and JNI parts:
$ mkdir device/marakana/alpha/framework/libmrknlog_jni $ mkdir device/marakana/alpha/framework/libmrknlog_jni/java $ mkdir device/marakana/alpha/framework/libmrknlog_jni/jni
To include these sub-directories in the compilation (java/ and jni/), we again need one of those "recursive" makefiles:
include $(call all-subdir-makefiles)
Now, let’s create the directory structure that reflects our Java library’s package name (com.marakana.android.lib.log):
$ mkdir -p device/marakana/alpha/framework/libmrknlog_jni/java/com/marakana/android/lib/log
Now let’s create our Java "library" class (LibLog.java) and its accompanying exception (LibLogException.java):
package com.marakana.android.lib.log; public class LibLog { public native static void flushLog() throws LibLogException; public native static int getTotalLogSize() throws LibLogException; public native static int getUsedLogSize() throws LibLogException; static { System.loadLibrary("mrknlog_jni"); } }
package com.marakana.android.lib.log; public class LibLogException extends RuntimeException { public LibLogException(String msg) { super(msg); } }
While we are at it, we might as well create a simple Main.java class to test our Java/JNI library:
package com.marakana.android.lib.log; /** @hide */ public class Main { public static void main (String[] args) { try { int usedSize = LibLog.getUsedLogSize(); int totalSize = LibLog.getTotalLogSize(); LibLog.flushLog(); System.out.printf("Flushed log. Previously it was consuming %d of %d bytes\n", usedSize, totalSize); } catch (LibLogException e) { System.err.println("Failed to flush the log"); e.printStackTrace(); } } }
For our Java/JNI library (deployed as /system/framework/com.marakana.android.lib.log.jar) to be accessible to the running applications, we need to explicitly expose its logical name (com.marakana.android.lib.log) via a simple XML mapping file (com.marakana.android.lib.log.xml):
<?xml version="1.0" encoding="utf-8"?> <permissions> <library name="com.marakana.android.lib.log" file="/system/framework/com.marakana.android.lib.log.jar"/> </permissions>
|
|
This file will be deployed as /system/etc/permissions/com.marakana.android.lib.log.xml in the target image and applications that wish to use our library will have to reference it in their via AndroidManifest.xml with: <uses-library android:name="com.marakana.android.lib.log" android:required="true"/> |
Now we are ready for the makefile (Android.mk):
LOCAL_PATH := $(call my-dir) # Build the library include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := com.marakana.android.lib.log LOCAL_SRC_FILES := $(call all-java-files-under,.) include $(BUILD_JAVA_LIBRARY) # Build the documentation include $(CLEAR_VARS) LOCAL_SRC_FILES := $(call all-subdir-java-files) $(call all-subdir-html-files) LOCAL_MODULE:= com.marakana.android.lib.log_doc LOCAL_DROIDDOC_OPTIONS := com.marakana.android.lib.log LOCAL_MODULE_CLASS := JAVA_LIBRARIES LOCAL_DROIDDOC_USE_STANDARD_DOCLET := true include $(BUILD_DROIDDOC) # Copy com.marakana.android.lib.log.xml to /system/etc/permissions/ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := com.marakana.android.lib.log.xml LOCAL_MODULE_CLASS := ETC LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/permissions LOCAL_SRC_FILES := $(LOCAL_MODULE) include $(BUILD_PREBUILT)
|
|
In this one file, we have three separate targets: to compile the code, to build the documentation (used by the SDK addon), and to copy the com.marakana.android.lib.log.xml file to /system/etc/permissions/ (though this last one seems a too fancy for a simple copy command). Notice that we separate these three targets using the include $(CLEAR_VARS) macro. |
Compile:
$ cd device/marakana/alpha/framework/libmrknlog_jni/java device/marakana/alpha/framework/libmrknlog_jni/java$ mm com.marakana.android.lib.log com.marakana.android.lib.log.xml … target Java: com.marakana.android.lib.log … Copying: out/target/common/obj/JAVA_LIBRARIES/com.marakana.android.lib.log_intermediates/classes.jar … Install: out/target/product/alpha/system/framework/com.marakana.android.lib.log.jar target Prebuilt: com.marakana.android.lib.log.xml (out/target/product/alpha/obj/ETC/com.marakana.android.lib.log.xml_intermediates/com.marakana.android.lib.log.xml) Install: out/target/product/alpha/system/etc/permissions/com.marakana.android.lib.log.xml … device/marakana/alpha/framework/libmrknlog_jni/java$ croot $
Next, we need create the C header file for our library (using the javah -jni command), and fortunately we can re-use the compiled classes left for us in the out/ directory by the previous step:
$ javah -jni \
-d device/marakana/alpha/framework/libmrknlog_jni/jni/ \
-classpath out/target/common/obj/JAVA_LIBRARIES/com.marakana.android.lib.log_intermediates/classes.jar \
com.marakana.android.lib.log.LibLog
To check that it worked, we can take a look at the generated C header file (…_LibLog.h):
… JNIEXPORT void JNICALL Java_com_marakana_android_lib_log_LibLog_flushLog (JNIEnv *, jclass); … JNIEXPORT jint JNICALL Java_com_marakana_android_lib_log_LibLog_getTotalLogSize (JNIEnv *, jclass); … JNIEXPORT jint JNICALL Java_com_marakana_android_lib_log_LibLog_getUsedLogSize (JNIEnv *, jclass); …
Now we can provide our implementation (….LibLog.c), which simply wraps calls to functions from libmrknlog and provides some basic error handling:
#include <mrknlog.h> #include "com_marakana_android_lib_log_LibLog.h" static void ThrowLibLogException(JNIEnv *env, const char *message) { jclass class = (*env)->FindClass(env, "com/marakana/android/lib/log/LibLogException"); if (class) { (*env)->ThrowNew(env, class, message); (*env)->DeleteLocalRef(env, class); } } JNIEXPORT void JNICALL Java_com_marakana_android_lib_log_LibLog_flushLog (JNIEnv *env, jclass clazz) { if (mrkn_flush_log() != 0) { ThrowLibLogException(env, "Failed to flush log"); } } JNIEXPORT jint JNICALL Java_com_marakana_android_lib_log_LibLog_getTotalLogSize (JNIEnv *env, jclass clazz) { jint result = mrkn_get_total_log_size(); if (result < 0) { ThrowLibLogException(env, "Failed to get total log size"); } return result; } JNIEXPORT jint JNICALL Java_com_marakana_android_lib_log_LibLog_getUsedLogSize (JNIEnv *env, jclass clazz) { jint result = mrkn_get_used_log_size(); if (result < 0) { ThrowLibLogException(env, "Failed to get used log size"); } return result; }
Next, we create a makefile (Android.mk) to compile our JNI code into a shared library (libmrknlog_jni.so):
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := com_marakana_android_lib_log_LibLog.c LOCAL_C_INCLUDES += $(JNI_H_INCLUDE) $(LOCAL_PATH)/../../../include/ LOCAL_SHARED_LIBRARIES := libmrknlog LOCAL_MODULE := libmrknlog_jni include $(BUILD_SHARED_LIBRARY)
|
|
As with libmrknlog.so, in previous versions of Android (pre-4.0) we would have had to also specify LOCAL_PRELINK_MODULE := false to disable pre-linking of our shared library. |
Now we have all the pieces to compile our JNI library (into /system/lib/libmrknlog_jni.so):
$ cd device/marakana/alpha/framework/libmrknlog_jni/jni device/marakana/alpha/framework/libmrknlog_jni/jni$ mm … Install: out/target/product/alpha/system/lib/libmrknlog_jni.so device/marakana/alpha/framework/libmrknlog_jni/jni$ croot $
As before, since our Java/JNI library’s components are marked as LOCAL_MODULE_TAGS := optional, in order to get them into the final ROM, we need to register them with Alpha’s PRODUCT_PACKAGES:
… PRODUCT_PACKAGES += \ com.marakana.android.lib.log \ com.marakana.android.lib.log.xml \ libmrknlog_jni
Now we can rebuild our entire device:
$ make -j10 … Install system fs image: out/target/product/alpha/system.img
Finally, we are ready to test our Java/JNI library via the Main.main() method:
# (re)start the emulator $ out/host/linux-x86/bin/emulator -kernel out/target/product/alpha/kernel & # wait for the emulator to finish # check out our Java library $ adb shell ls -l /system/framework/com.marakana.android.lib.log.jar -rw-r--r-- root root 313 2012-01-05 00:47 com.marakana.android.lib.log.jar # check out our Java library registry file $ adb shell ls -l /system/etc/permissions/com.marakana.android.lib.log.xml -rw-r--r-- root root 180 2012-01-05 00:47 com.marakana.android.lib.log.xml # check out our JNI shared library $ adb shell ls -l /system/lib/libmrknlog_jni.so -rw-r--r-- root root 5452 2012-01-05 00:54 libmrknlog_jni.so # check if our utility is doing what it is supposed to $ adb logcat -g /dev/log/main: ring buffer is 64Kb (33Kb consumed), max entry is 4096b, max payload is 4076b # now run our Java library's Main.main() by directly invoking the Dalvik VM $ adb shell dalvikvm -cp /system/framework/com.marakana.android.lib.log.jar com.marakana.android.lib.log.Main Flushed log. Previously it was consuming 34346 of 65536 bytes # check again $ adb logcat -g /dev/log/main: ring buffer is 64Kb (0Kb consumed), max entry is 4096b, max payload is 4076b $ adb shell dalvikvm -cp /system/framework/com.marakana.android.lib.log.jar com.marakana.android.lib.log.Main Flushed log. Previously it was consuming 318 of 65536 bytes # good :-)
Create a home directory for our Alpha apps:
$ mkdir -p device/marakana/alpha/app/
|
|
This app will not be shared with the SDK add-on. |
As we should know by now, we need a makefile (Android.mk) to include app/'s the sub-directories:
include $(call all-subdir-makefiles)
Now we can create a sub-directory for our app (MrknLogLibClient), its basic directory structure (res/, and src/), and the source package (com.marakana.android.loglibclient) directory structure:
$ mkdir -p device/marakana/alpha/app/MrknLogLibClient/res/values $ mkdir -p device/marakana/alpha/app/MrknLogLibClient/res/layout $ mkdir -p device/marakana/alpha/app/MrknLogLibClient/src/com/marakana/android/loglibclient
We need some basic string resources (used in the UI):
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Marakana Log Lib Client</string> <string name="log_utilization_message">Using %1$d of %2$d bytes of the log buffer</string> <string name="flush_log_button">Flush Log Buffer</string> </resources>
And we also need a simple layout (log.xml), with a text view, to show the current log utilization, and a button, to allow us to clear the log:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:id="@+id/output" /> <Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/button" android:text="@string/flush_log_button"/> </LinearLayout>
Now, we are ready to write our one-and only activity (LogActivity) utilizing com.marakana.android.lib.log Java/JNI library:
package com.marakana.android.loglibclient; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.marakana.android.lib.log.LibLog; public class LogActivity extends Activity implements View.OnClickListener, Runnable { private TextView output; private Handler handler; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); super.setContentView(R.layout.log); this.output = (TextView) super.findViewById(R.id.output); Button button = (Button) super.findViewById(R.id.button); button.setOnClickListener(this); this.handler = new Handler(); } private void updateOutput() { this.output.setText( super.getString(R.string.log_utilization_message, LibLog.getUsedLogSize(), LibLog.getTotalLogSize())); } @Override public void onResume() { super.onResume(); this.handler.post(this); } @Override public void onPause() { super.onPause(); this.handler.removeCallbacks(this); } public void onClick(View view) { LibLog.flushLog(); this.updateOutput(); } public void run() { this.updateOutput(); this.handler.postDelayed(this, 1000); } }
|
|
Our activity uses a handler in order to request periodic (1 Hz) call-backs to the run() method, via which we simply update our UI with the log utilization data. |
And, like any other app, we need to provide our own AndroidManifest.xml configuration file:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.loglibclient" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="8" /> <uses-permission android:name="android.permission.READ_LOGS"/> <application android:label="@string/app_name"> <uses-library android:name="com.marakana.android.lib.log" android:required="true"/> <activity android:name=".LogActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
|
|
We need <uses-permission android:name="android.permission.READ_LOGS"/> so that our app’s user ID gets added to the log group membership, or otherwise we won’t be able to read from /dev/log/main (as discussed earlier). Additionally, we added <uses-library android:name="com.marakana.android.lib.log" android:required="true"/> so that we get run-time access to the com.marakana.android.lib.log library. |
And, like any other component, our app also needs its own makefile (Android.mk):
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under,src) LOCAL_JAVA_LIBRARIES := com.marakana.android.lib.log LOCAL_PACKAGE_NAME := MrknLogLibClient LOCAL_SDK_VERSION := current LOCAL_PROGUARD_ENABLED := disabled include $(BUILD_PACKAGE)
|
|
We are referencing our com.marakana.android.lib.log library here as well, for the sake of the compiler. Also, notice that we are using include $(BUILD_PACKAGE) in order to build our code as an app. |
And, also like any other component that we wish to include in our ROM, we need to register MrknLogLibClient with the device’s main makefile (full_alpha.xml):
…
PRODUCT_PACKAGES += MrknLogLibClientNow we can compile it:
$ make -j10 … Install: out/target/product/alpha/system/app/MrknLogLibClient.apk … Install system fs image: out/target/product/alpha/system.img
And finally, we can test everything:
Restart the emulator
Launch the Marakana Log Lib Client app
Test that you can flush the log buffer
We’ll start off by creating a directory structure for the custom manager library, since that’s where our service descriptor (ILogService.aidl) is going to live:
$ mkdir device/marakana/alpha/framework/mrknlogservice $ mkdir -p device/marakana/alpha/framework/mrknlogservice/com/marakana/android/service/log
Let’s create a simple AIDL description of our service (LibLogService.aidl):
package com.marakana.android.service.log; /** * System-private API for talking to the LogService. * * {@hide} */ interface ILogService { void flushLog(); int getTotalLogSize(); int getUsedLogSize(); }
|
|
Notice that the methods described by ILogService.aidl closely match what our LibLog JNI-bridge provides. Also, we are using {@hide} to hide our AIDL interface from the documentation we’ll be producing later for our SDK addon. Why hide it? Because the clients will use our service via a manager proxy that we’ll be creating next. |
We don’t want to force the "complexity" of Binder upon our clients, so we provide them with a convenience LogManager proxy to our (yet-to-be-created) service:
package com.marakana.android.service.log; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; public class LogManager { private static final String TAG = "LogManager"; private static final String REMOTE_SERVICE_NAME = ILogService.class.getName(); private final ILogService service; public static LogManager getInstance() { return new LogManager(); } private LogManager() { Log.d(TAG, "Connecting to ILogService by name [" + REMOTE_SERVICE_NAME + "]"); this.service = ILogService.Stub.asInterface(ServiceManager.getService(REMOTE_SERVICE_NAME)); if (this.service == null) { throw new IllegalStateException("Failed to find ILogService by name [" + REMOTE_SERVICE_NAME + "]"); } } public void flushLog() { try { Log.d(TAG, "Flushing logs. If it works, you won't see this message."); this.service.flushLog(); } catch (RemoteException e) { throw new RuntimeException("Failed to flush log", e); } } public int getTotalLogSize() { try { return this.service.getTotalLogSize(); } catch (RemoteException e) { throw new RuntimeException("Failed to get total log size", e); } } public int getUsedLogSize() { try { return this.service.getUsedLogSize(); } catch (Exception e) { throw new RuntimeException("Failed to get used log size", e); } } }
|
|
We are using android.os.ServiceManager to lookup an object providing ILogService implementation. This is going to be our service, which we yet have to create and register with the service-manager. |
For our clients to access LogManager, we need to expose it as a Java library - so we create an XML descriptor for it:
<?xml version="1.0" encoding="utf-8"?> <permissions> <library name="com.marakana.android.service.log" file="/system/framework/com.marakana.android.service.log.jar"/> </permissions>
Now we are ready ready to create a makefile (Android.mk) with rules to compile our library, build its documentation, and copy its com.marakana.android.service.log.xml descriptor to /system/etc/permissions/:
LOCAL_PATH := $(call my-dir) # Build the library include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := com.marakana.android.service.log LOCAL_SRC_FILES := $(call all-java-files-under,.) LOCAL_SRC_FILES += com/marakana/android/service/log/ILogService.aidl include $(BUILD_JAVA_LIBRARY) # Build the documentation include $(CLEAR_VARS) LOCAL_SRC_FILES := $(call all-subdir-java-files) $(call all-subdir-html-files) LOCAL_MODULE:= com.marakana.android.service.log_doc LOCAL_DROIDDOC_OPTIONS := com.marakana.android.service.log LOCAL_MODULE_CLASS := JAVA_LIBRARIES LOCAL_DROIDDOC_USE_STANDARD_DOCLET := true include $(BUILD_DROIDDOC) # Copy com.marakana.android.service.log.xml to /system/etc/permissions/ include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_MODULE := com.marakana.android.service.log.xml LOCAL_MODULE_CLASS := ETC LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/permissions LOCAL_SRC_FILES := $(LOCAL_MODULE) include $(BUILD_PREBUILT)
To test that we did everything correctly, we can compile our library:
$ cd device/marakana/alpha/framework/mrknlogservice device/marakana/alpha/framework/mrknlogservice$ mm -j10 com.marakana.android.service.log com.marakana.android.service.log.xml … Aidl: com.marakana.android.service.log <= device/marakana/alpha/framework/mrknlogservice/com/marakana/android/service/log/ILogService.aidl … Install: out/target/product/alpha/system/framework/com.marakana.android.service.log.jar … Install: out/target/product/alpha/system/etc/permissions/com.marakana.android.service.log.xml … device/marakana/alpha/framework/mrknlogservice$ croot $
For our com.marakana.android.service.log library to be included in the final ROM, we need to add it to the common.mk makefile:
… PRODUCT_PACKAGES += \ com.marakana.android.service.log \ com.marakana.android.service.log.xml
Now we can create our MrknLogService, which will implement ILogService, but since we’ll define our service as an application, we need first to create a directory for alpha apps:
$ mkdir -p device/marakana/alpha/app/
|
|
This directory may already exist if we created MrknLogLibClient app before (this was optional). |
And, as we know by now, we need a makefile to include app/'s sub-directories into the build:
include $(call all-subdir-makefiles)
|
|
This file may already exist if we created MrknLogLibClient app before (this was optional). |
Next, we create a directory for our service (MrknLogService), its basic directory structure (res/, and src/), and the source package (com.marakana.android.loglibclient) directory structure:
$ mkdir -p device/marakana/alpha/app/MrknLogService/res/values $ mkdir -p device/marakana/alpha/app/MrknLogService/src/com/marakana/android/logservice
We need some basic string resources:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="flush_log_permission_label">Flush Log</string> <string name="flush_log_permission_description">Applications with this permission will be able to clear the logs, potentially covering their own tracks of malicious behavior.</string> </resources>
|
|
These strings are used for our custom permission that we’ll be creating next. |
Next, let’s define our app’s AndroidManifest.xml file:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.logservice" android:versionCode="1" android:versionName="1.0" android:sharedUserId="android.uid.system"> <uses-sdk android:minSdkVersion="8" /> <uses-permission android:name="android.permission.READ_LOGS"/> <application android:name=".LogServiceApp" android:persistent="true"> <uses-library android:name="com.marakana.android.service.log" android:required="true"/> <uses-library android:name="com.marakana.android.lib.log" android:required="true"/> </application> <permission android:name="com.marakana.android.logservice.FLUSH_LOG" android:protectionLevel="dangerous" android:permissionGroup="android.permission-group.SYSTEM_TOOLS" android:label="@string/flush_log_permission_label" android:description="@string/flush_log_permission_description"/> </manifest>
|
|
In this manifest file, there are a few items of interest: We required that our app run with the system user (so that we can access the ServiceManager ⇒ /system/bin/servicemanager) by adding android:sharedUserId="android.uid.system" attribute to the manifest. This is enforced by frameworks/base/cmds/servicemanager/service_manager.c:svc_can_register(…). We gave ourselves access to the log group (so that we can read from /dev/log/main) with the <uses-permission android:name="android.permission.READ_LOGS"/> entry. We defined a custom LogServiceApp application class, which once loaded, will in turn load our service (ILogServiceImpl) and register it with the ServiceManager. We defined our application as android:persistent="true", which means that the ActivityManager will automatically launch it on boot, and it will never demote its importance (/proc/<our-service-app-pid>/oom_adj=-12), so ActivityManagerService and the low memory killer will never kill it. Additionally, gave our application access to com.marakana.android.service.log library to get access to ILogService interface and com.marakana.android.lib.log library to get access to LibLog JNI bridge. Finally, we defined our custom permission (….FLUSH_LOG), which we’ll later enforce in our service class implementation (ILogServiceImpl). |
Now we can provide the implementation ILogServiceImpl for our AIDL-defined interface (ILogService) we created earlier:
package com.marakana.android.logservice; import android.content.Context; import android.content.pm.PackageManager; import android.os.RemoteException; import android.util.Log; import com.marakana.android.service.log.ILogService; import com.marakana.android.lib.log.LibLog; class ILogServiceImpl extends ILogService.Stub { private static final String TAG = "ILogServiceImpl"; private final Context context; ILogServiceImpl(Context context) { this.context = context; } public void flushLog() throws RemoteException { if (this.context.checkCallingOrSelfPermission(Manifest.permission.FLUSH_LOG) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires FLUSH_LOG permission"); } Log.d(TAG, "Flushing logs. If it works, you won't see this message."); LibLog.flushLog(); } public int getUsedLogSize() throws RemoteException { return LibLog.getUsedLogSize(); } public int getTotalLogSize() throws RemoteException { return LibLog.getTotalLogSize(); } }
|
|
For the most part, our service simply wraps our JNI library, except that it requires that the caller be granted our custom Manifest.permission.FLUSH_LOG permission before calling the LibLog.flushLog() method. |
For our ILogServiceImpl to be accessible to our LogManager, we need to instantiate it and register it with the ServiceManager - we do this in our custom LogServiceApp application class loaded from the manifest file:
package com.marakana.android.logservice; import android.app.Application; import android.os.ServiceManager; import android.util.Log; import com.marakana.android.service.log.ILogService; public class LogServiceApp extends Application { private static final String TAG = "LogServiceApp"; private static final String REMOTE_SERVICE_NAME = ILogService.class.getName(); private ILogServiceImpl serviceImpl; public void onCreate() { super.onCreate(); this.serviceImpl = new ILogServiceImpl(this); ServiceManager.addService(REMOTE_SERVICE_NAME, this.serviceImpl); Log.d(TAG, "Registered [" + serviceImpl.getClass().getName() + "] as [" + REMOTE_SERVICE_NAME + "]"); } public void onTerminate() { super.onTerminate(); Log.d(TAG, "Terminated"); } }
|
|
Normally, regular applications cannot get access to ServiceManager class (because it is hidden) nor to the servicemanager daemon (because it enforces UID-based restrictions). To work around these limitations, we’ll compile our app with the framework classes (where ServiceManager is not hidden), and run it with the system user (which does have access to servicemanager daemon). |
We are now ready for our makefile (Android.mk):
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under,src) LOCAL_REQUIRED_MODULES := \ com.marakana.android.service.log \ com.marakana.android.lib.log LOCAL_JAVA_LIBRARIES := \ com.marakana.android.service.log \ com.marakana.android.lib.log \ core \ framework LOCAL_PACKAGE_NAME := MrknLogService LOCAL_SDK_VERSION := current LOCAL_PROGUARD_ENABLED := disabled LOCAL_CERTIFICATE := platform include $(BUILD_PACKAGE)
|
|
The setting LOCAL_JAVA_LIBRARIES := … framework allows us to reference ServiceManager and LOCAL_CERTIFICATE := platform makes it possible for us to run with the system user and thereby access the servicemanager daemon. |
Let’s compile our code:
$ cd device/marakana/alpha/app/MrknLogService device/marakana/alpha/app/MrknLogService$ mm … Install: out/target/product/alpha/system/app/MrknLogService.apk device/marakana/alpha/app/MrknLogService$ croot $
We want our MrknLogService to be included in the final ROM, so we add it to common.mk:
…
PRODUCT_PACKAGES += MrknLogServiceCompile the entire device:
$ make -j10 … Install system fs image: out/target/product/alpha/system.img
It’s best to test our new manager→service→library→driver proxy via a real application, so that’s what we’ll be creating next.
Create a new directory for our new app (MrknLogServiceClient), its basic directory structure (res/, and src/), and the source package (com.marakana.android.logserviceclient) directory structure:
$ mkdir -p device/marakana/alpha/app/MrknLogServiceClient/res/values $ mkdir -p device/marakana/alpha/app/MrknLogServiceClient/res/layout $ mkdir -p device/marakana/alpha/app/MrknLogServiceClient/src/com/marakana/android/logserviceclient
Just like before, we need some basic string resources (used in the UI):
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Marakana Log Service Client</string> <string name="log_utilization_message">Using %1$d of %2$d bytes of the log buffer</string> <string name="flush_log_button">Flush Log Buffer</string> </resources>
And we also need a simple layout (log.xml), which is exactly the same as the one we created for MrknLogLibClient before:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:id="@+id/output" /> <Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/button" android:text="@string/flush_log_button"/> </LinearLayout>
Our activity (LogActivity) will be similar to the one we wrote before, except that in this case will be using com.marakana.android.service.log Java (Binder) library and its LogManager APIs:
package com.marakana.android.logserviceclient; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView; import com.marakana.android.service.log.LogManager; public class LogActivity extends Activity implements Runnable, OnClickListener { private TextView output; private Handler handler; private LogManager logManager; public void onCreate(Bundle savedInstanceState) { this.logManager = LogManager.getInstance(); super.onCreate(savedInstanceState); super.setContentView(R.layout.log); this.output = (TextView)super.findViewById(R.id.output); Button button = (Button)super.findViewById(R.id.button); button.setOnClickListener(this); this.handler = new Handler(); } private void updateOutput() { this.output.setText(super.getString(R.string.log_utilization_message, this.logManager.getUsedLogSize(), this.logManager.getTotalLogSize())); } @Override public void onResume() { super.onResume(); this.handler.post(this); } @Override public void onPause() { super.onPause(); this.handler.removeCallbacks(this); } public void onClick(View view) { this.logManager.flushLog(); this.updateOutput(); } public void run() { this.updateOutput(); this.handler.postDelayed(this, 1000); } }
Like before, we need a AndroidManifest.xml file, but this time around we are using the ….FLUSH_LOG permission, and com.marakana.android.service.log library:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.marakana.android.logserviceclient" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="8" /> <uses-permission android:name="com.marakana.android.logservice.FLUSH_LOG" /> <application android:label="@string/app_name"> <uses-library android:name="com.marakana.android.service.log" android:required="true" /> <activity android:name=".LogActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
And, also like before, our app needs its own makefile (Android.mk):
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under,src) LOCAL_JAVA_LIBRARIES := com.marakana.android.service.log LOCAL_PACKAGE_NAME := MrknLogServiceClient LOCAL_SDK_VERSION := current LOCAL_PROGUARD_ENABLED := disabled include $(BUILD_PACKAGE)
|
|
We are referencing our com.marakana.android.service.log library here as well, for the sake of the compiler. |
And, as before, we need to register MrknLogServiceClient with the device’s main makefile (full_alpha.xml) in order to get it included in the ROM:
…
PRODUCT_PACKAGES += MrknLogServiceClientNow we can compile our entire device:
$ make -j10 … Install: out/target/product/alpha/system/app/MrknLogServiceClient.apk … Install system fs image: out/target/product/alpha/system.img
And finally, we can test everything:
Restart the emulator
Launch the Marakana Log Service Client app
Test that you can flush the log buffer
Create a directory for our addon:
$ mkdir device/marakana/alpha_sdk_addon
Though this is purely optional, let’s create a custom emulator skin for our add-on, by copying an existing one:
$ mkdir device/marakana/alpha_sdk_addon/skins $ cp -r sdk/emulator/skins/HVGA device/marakana/alpha_sdk_addon/skins/MrknHvgaMdpi
We could now customize the portrait background of our skin (device/marakana/alpha_sdk_addon/skins/MrknHvgaMdpi/background_port.png) - say by adding our custom logo to it
|
|
For example, we could use this one: https://github.com/marakana/alpha_sdk_addon/raw/master/skins/MrknHvgaMdpi/background_land.png |
We could also customize the landscape background of our skin (device/marakana/alpha_sdk_addon/skins/MrknHvgaMdpi/background_land.png) - also by adding our custom logo to it
|
|
For example, we could use this one: https://github.com/marakana/alpha_sdk_addon/raw/master/skins/MrknHvgaMdpi/background_port.png |
We could also customize other images, layout controls (layout), and hardware-specific values (hardware.ini)
To simulate that our devices supports NFC, we would:
Create a directory (nxp):
$ mkdir device/marakana/alpha_sdk_addon/nxp
Add a configuration file (com.nxp.mifare.xml) declaring NFC-feature support:
<?xml version="1.0" encoding="utf-8"?> <!-- Devices that support MIFARE need to declare NXP's feature constant --> <permissions> <feature name="com.nxp.mifare" /> </permissions>
|
|
NFC-support is not really required for our particular product (Alpha), but this is an example of how some hardware-specific features are defined. |
While on the subject of hardware, let’s define skin-independent set of hardware properties (hardware.ini) for our emulated device:
# Custom hardware options for the add-on. # Properties defined here impact all AVD targeting this add-on. # Each skin can also override those values with its own hardware.ini file. vm.heapSize = 24
Next, we define the properties of our SDK addon (manifest.ini) - name, description, API version, revision, libraries, and skin - as these properties are required by development tools (such as SDK Manager and Eclipse) using our addon:
name=Alpha Add-On vendor=Marakana description=Marakana Alpha Add-on api=15 revision=1 libraries=com.marakana.android.lib.log;com.marakana.android.service.log com.marakana.android.lib.log=com.marakana.android.lib.log.jar;Marakana Log Library com.marakana.android.service.log=com.marakana.android.service.log.jar;Marakana Log Service skin=MrknHvgaMdpi
Next, we define a list of classes to be included (+<package-name>.(*|<class-name>)) or excluded (-<package-name>.(\*|<class-name>)) from the generated SDK addon’s libraries:
+com.marakana.android.lib.log.* -com.marakana.android.lib.log.Main +com.marakana.android.service.log.*
|
|
These classes are known as SDK Addon Stub Definitions, and this file is processed by development/tools/mkstubs during the build time. The Java tool mkstubs loads this file via PRODUCT_SDK_ADDON_STUB_DEFS makefile property. |
Now we are ready for our addon’s main makefile (alpha_sdk_addon.mk), which will be loaded later by AndroidProducts.mk:
# Include the common stuff include $(LOCAL_PATH)/../alpha/common.mk # List of modules to include in the the add-on system image #PRODUCT_PACKAGES += # The name of this add-on (for the SDK) PRODUCT_SDK_ADDON_NAME := marakana_alpha_addon # Copy the following files for this add-on's SDK PRODUCT_SDK_ADDON_COPY_FILES := \ $(LOCAL_PATH)/manifest.ini:manifest.ini \ $(LOCAL_PATH)/hardware.ini:hardware.ini \ $(call find-copy-subdir-files,*,$(LOCAL_PATH)/skins/MrknHvgaMdpi,skins/MrknHvgaMdpi) # Copy the jar files for the libraries (APIs) exposed in this add-on's SDK PRODUCT_SDK_ADDON_COPY_MODULES := \ com.marakana.android.lib.log:libs/com.marakana.android.lib.log.jar \ com.marakana.android.service.log:libs/com.marakana.android.service.log.jar PRODUCT_SDK_ADDON_STUB_DEFS := $(LOCAL_PATH)/alpha_sdk_addon_stub_defs.txt # Define the name of the documentation to generate for this add-on's SDK PRODUCT_SDK_ADDON_DOC_MODULES := \ com.marakana.android.service.log_doc # Since the add-on is an emulator, we also need to explicitly copy the kernel to images PRODUCT_SDK_ADDON_COPY_FILES += $(LOCAL_KERNEL):images/armeabi-v7a/kernel-qemu # This add-on extends the default sdk product. $(call inherit-product, $(SRC_TARGET_DIR)/product/sdk.mk) # The name of this add-on (for the build system) # Use 'make PRODUCT-<PRODUCT_NAME>-sdk_addon' to build the an add-on, # so in this case, we would run 'make PRODUCT-marakana_alpha_addon-sdk_addon' PRODUCT_NAME := marakana_alpha_addon PRODUCT_DEVICE := alpha PRODUCT_MODEL := Marakana Alpha SDK Addon Image for Emulator
|
|
As we can see from the comments, this file extends from build/target/product/sdk.mk, includes everything from alpha-commmon, adds support for library documentation, defines the addon’s name (PRODUCT_SDK_ADDON_NAME), specifies the files/modules to be copied (manifest.ini, hardware.ini, skin, libraries, and kernel) into the addon (PRODUCT_SDK_ADDON_COPY_FILES), loads stub defs, and defines the addon’s product info (like PRODUCT_NAME). |
Just like with our alpha product, we need to create AndroidProducts.mk file, which the build system looks for to get a list of the actual make files (in this just alpha_sdk_addon.mk) to process for this "product" (i.e. SDK addon):
PRODUCT_MAKEFILES := $(LOCAL_DIR)/alpha_sdk_addon.mk
Just to be on the safe side, let’s make sure that make files from all sub-directories also get included:
include $(call all-subdir-makefiles)
|
|
This file is unnecessary in this particular case, but it does not hurt to have it in case we add more addon-specific components down the road, which will most likely be stored in their own sub-directories. |
Now we are ready to compile our addon:
$ make -j10 PRODUCT-marakana_alpha_addon-sdk_addon … Copy: out/host/linux-x86/obj/SDK_ADDON/marakana_alpha_addon_intermediates/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/system.img Packaging SDK Addon: out/host/linux-x86/sdk_addon/marakana_alpha_addon-eng.sasa-linux-x86.zip
|
|
Notice that the directory/file-name encodes the name of the user who built this addon (in this case "sasa"). Adjust the following steps as necessary. |
Next, to test that it works, we first install our SDK to our Android SDK’s add-ons/ directory:
$ unzip out/host/linux-x86/sdk_addon/marakana_alpha_addon-eng.sasa-linux-x86.zip -d /home/sasa/android/sdk/add-ons/ Archive: out/host/linux-x86/sdk_addon/marakana_alpha_addon-eng.sasa-linux-x86.zip extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/select.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/arrow_left.png inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/background_land.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/arrow_right.png inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/layout inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/background_port.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/hardware.ini inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/spacebar.png inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/controls.png inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/key.png inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/keyboard.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/button.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/arrow_down.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/skins/MrknHvgaMdpi/arrow_up.png extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/package-list inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/help-doc.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/com/marakana/android/service/log/package-summary.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/com/marakana/android/service/log/package-frame.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/com/marakana/android/service/log/package-tree.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/com/marakana/android/service/log/LogManager.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/allclasses-noframe.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/constant-values.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/stylesheet.css inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/deprecated-list.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/index.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/allclasses-frame.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/index-all.html extracting: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/resources/inherit.gif inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/docs/com.marakana.android.service.log_doc/overview-tree.html inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/build.prop inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/ramdisk.img inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/kernel-qemu inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/NOTICE.txt inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/userdata.img inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/images/armeabi-v7a/system.img inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/hardware.ini inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/manifest.ini inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/libs/com.marakana.android.lib.log.jar inflating: /home/sasa/android/sdk/add-ons/marakana_alpha_addon-eng.sasa-linux-x86/libs/com.marakana.android.service.log.jar
|
|
Adjust the path to Android SDK directory as necessary. |
Let’s test that it shows up in our Android SDK and ADV Manager:
Run
$ /path/to/android/sdk/tools/android $
|
|
Adjust the path to Android SDK directory as necessary. |
And we should now see "Alpha Add-On by Marakana, Android API 15, revision 1, Installed" show up under Packages:
|
|
For this to work, we need to have our addon’s api setting (in manifest.ini file) match an available SDK Platform Android of the same API version. |
Now we can create a new emulator image (AVD) based on our add-on (from the Android SDK and AVD Manager):
To see that it works, we can now launch our newly created "marakana-alpha" AVD:
Our marakana-alpha AVD will not have any of the Mrkn*ClientApp apps we previously created for the "Marakana Alpha" device , but since our APIs are available to us via the libraries, we can re-create these applications in Eclipse and deploy them to our AVD - this is left as an exercise for the reader :-)
First, we need to create a repository.xml file to describe our add-on and publish it to our server:
<?xml version="1.0" encoding="UTF-8"?> <sdk:sdk-addon xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sdk="http://schemas.android.com/sdk/android/addon/1"> <sdk:add-on> <sdk:name>Alpha Add-On</sdk:name> <sdk:api-level>15</sdk:api-level> <sdk:vendor>Marakana</sdk:vendor> <sdk:revision>1</sdk:revision> <sdk:description>Android + Marakana Alpha Add-on, API 15, revision 1 </sdk:description> <sdk:desc-url>http://marakana.com/external/android/sdk-addon/</sdk:desc-url> <sdk:uses-license ref="marakana-android-addon-license" /> <sdk:archives> <sdk:archive os="any"> <sdk:size>96382546</sdk:size> <sdk:checksum type="sha1">5cec8cc3f3064441cb96a50e2b8aa528681ffc78</sdk:checksum> <sdk:url>marakana_alpha_sdk_addon_api-15_r1.zip</sdk:url> </sdk:archive> </sdk:archives> <sdk:libs> </sdk:libs> </sdk:add-on> <sdk:license type="text" id="marakana-android-addon-license"> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this SDK addon except in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. </sdk:license> </sdk:sdk-addon>
|
|
We have to make sure that we adjust the info as necessary to reflect our particular site/addon. |
Next, we need to upload our addon to our site, relative to <sdk:sdk-addon> → <sdk:add-on> → <sdk:desc-url> URL (e.g. https://marakana.com/external/android/sdk-addon/marakana_alpha_sdk_addon_api-15_r1.zip)
We are now ready to test it out in our Android SDK Manager
Go to Tools → Manage Add-on Sites… in the menu bar
Click on New…
Enter the base URL in the URL field (e.g. https://marakana.com/external/android/sdk-addon/) and click on OK
Click on Close to dismiss the Add-on Sites window
Under Packages we should now see Alpha Add-On by Marakana, with a status Not installed
Click on the checkbox next to our package
Click on Install 1 package… button
Accept the license terms and click on Install
Test that it works by creating an AVD based on our add-on and/or using it in Eclipse
What is a home directory for a new custom board within AOSP source-tree?
What is the purpose of vendorsetup.sh?
What does $(call inherit-product, $(SRC_TARGET_DIR)/product/generic.mk) do?
What is the significance of PRODUCT_PACKAGES?
What is the purpose of BoardConfig.mk?
What is the purpose of development/tools/make_key and why do we want to use it?
What is the significance of /proc/cpuinfo?
When/why do we use include $(call all-subdir-makefiles)?
What is the purpose of LOCAL_MODULE?
What is the purpose of LOCAL_MODULE_TAGS?
How do we compile individual modules efficiently?
Name at least three include $(BUILD_XXX) targets.
What do we need to do in order to launch a custom daemon on startup?
How do we expose custom Java system libraries to applications?
How do applications consume custom Java system libraries?
When do we use include $(BUILD_PREBUILT)?
When/where do we use $(JNI_H_INCLUDE)?
What is the purpose of {@hide}?
When do we use include $(BUILD_DROIDDOC)?
What do we need to do in order to register a custom service to servicemanager?
When/why would we use LOCAL_JAVA_LIBRARIES += framework?
When/why would we use LOCAL_CERTIFICATE := platform?
What does include $(BUILD_PACKAGE) build?
What is SDK add-on and when/why would we want to build/use one?
How do we distribute custom SDK add-ons?
Android supports a variety of USB peripherals and Android USB accessories (hardware that implements the Android accessory protocol) through two modes: USB host and USB accessory.
In USB host mode, the Android-powered device acts as the host.
In USB accessory mode, the external USB hardware act as the USB hosts.
USB accessory and host modes are directly supported in Android 3.1 (API level 12) or newer platforms.
|
|
Support for USB host and accessory modes are ultimately dependent on the device’s hardware, regardless of platform level. You can filter for devices that support USB host and accessory through a <uses-feature> element, as discussed later. |
The android.hardware.usb package contains the following classes supporting USB host mode:
Allows you to enumerate and communicate with connected USB devices.
Represents a connected USB device and contains methods to access its identifying information, interfaces, and endpoints.
Represents an interface of a USB device, which defines a set of functionality for the device. A device can have one or more interfaces on which to communicate on.
Represents an interface endpoint, which is a communication channel for this interface. An interface can have one or more endpoints, and usually has input and output endpoints for two-way communication with the device.
Represents a connection to the device, which transfers data on endpoints. This class allows you to send data back and forth sychronously or asynchronously.
Represents an asynchronous request to communicate with a device through a UsbDeviceConnection.
Defines USB constants that correspond to definitions in linux/usb/ch9.h of the Linux kernel.
In general, you:
Obtain a UsbManager to retrieve the desired UsbDevice
Find the appropriate UsbInterface and the UsbEndpoint of that interface to communicate on
Open a UsbDeviceConnection to communicate with the USB device
In most situations, you need to use all of these classes (UsbRequest is only required if you are doing asynchronous communication) when communicating with a USB device.
Not all Android-powered devices are guaranteed to support the USB host APIs.
To indicate that your application requires USB host support, add the following elements to your application’s manifest:
<uses-feature android:name="android.hardware.usb.host" />
<uses-sdk android:minSdkVersion="12" />
When users connect USB devices to an Android-powered device, the Android system can determine whether your application is interested in the connected device. If so, you can set up communication with the device if desired.
To do this, your application has to:
Discover connected USB devices either by:
Ask the user for permission to connect to the USB device, if not already obtained.
Communicate with the USB device by reading and writing data on the appropriate interface endpoints.
To have your application discover a particular USB device, you can specify an intent filter to filter for the android.hardware.usb.action.USB_DEVICE_ATTACHED intent.
In your activity, you can obtain the UsbDevice that represents the attached device from the intent like this:
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
The following example shows how to declare the intent filter in your application’s manifest:
<activity ...> ... <intent-filter> <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" android:resource="@xml/device_filter" /> </activity>
The <meta-data> element points to an external XML resource file that declares identifying information about the device that you want to detect.
In the XML resource file, declare <usb-device> elements for the USB devices that you want to filter.
<?xml version="1.0" encoding="utf-8"?> <resources> <usb-device vendor-id="1234" product-id="5678" /> </resources>
Save the resource file in your application’s res/xml/ directory.
If your application is interested in inspecting all of the USB devices currently connected while your application is running, it can enumerate devices on the bus.
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); // ... HashMap<String, UsbDevice> deviceList = manager.getDeviceList(); UsbDevice device = deviceList.get("deviceName");
If desired, you can also just obtain an iterator from the hash map and process each device one by one:
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); // ... HashMap<String, UsbDevice> deviceList = manager.getDeviceList(); Iterator<UsbDevice> deviceIterator = deviceList.values().iterator(); while (deviceIterator.hasNext()) { UsbDevice device = deviceIterator.next() // ... }
Before communicating with the USB device, your application must have permission from your users.
|
|
If your application uses an intent filter to discover USB devices as they’re connected, it automatically receives permission if the user allows your application to handle the intent. If not, you must request permission explicitly in your application before connecting to the device. |
Explicitly asking for permission might be necessary in some situations such as when your application enumerates USB devices that are already connected and then wants to communicate with one.
To explicitly obtain permission:
Call UsbManager.requestPermission().
The call to requestPermission() displays a dialog to the user asking for permission to connect to the device.
The system generates a broadcast intent with a boolean EXTRA_PERMISSION_GRANTED extra indicating the user’s response.
The following sample code shows how to create a broadcast receiver to process the response:
private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"; private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { if (device != null) { // Call method to set up device communication } } else { Log.d(TAG, "permission denied for device " + device); } } } } };
To register the broadcast receiver, add this in your onCreate() method in your activity:
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); registerReceiver(mUsbReceiver, filter);
To display the dialog that asks users for permission to connect to the device, call the UsbManager.requestPermission() method:
UsbManager mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE); UsbDevice device; // ... mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0); mUsbManager.requestPermission(device, mPermissionIntent);
Communication with a USB device can be either synchronous or asynchronous.
To properly set up communication with a device, you need to obtain the appropriate UsbInterface and UsbEndpoint of the device that you want to communicate on and send requests on this endpoint with a UsbDeviceConnection. In general, your code should:
The following code snippet is a trivial way to do a synchronous data transfer. Your code should have more logic to correctly find the correct interface and endpoints to communicate on and also should do any transferring of data in a different thread than the main UI thread:
private byte[] bytes = "hello usb".getBytes(); private static int TIMEOUT = 0; private boolean forceClaim = true; ... UsbInterface intf = device.getInterface(0); UsbEndpoint endpoint = intf.getEndpoint(0); UsbDeviceConnection connection = mUsbManager.openDevice(device); connection.claimInterface(intf, forceClaim); connection.bulkTransfer(endpoint, bytes, bytes.length, TIMEOUT); // Do in another thread
To send data asynchronously, use the UsbRequest class to initialize and queue an asynchronous request, then wait for the result with requestWait().
When you are done communicating with a device or if the device was detached, close the UsbInterface and UsbDeviceConnection by calling releaseInterface() and close().
To listen for detached events, create a broadcast receiver like below:
BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (device != null) { // call your method that cleans up and closes communication with the device } } } };
Creating the broadcast receiver within the application, and not the manifest, allows your application to handle detached events only while it is running.
The USB accessory APIs were introduced to the platform in Android 3.1. They are also available in Android 2.3.4 using the Google APIs add-on library.
To support USB accessory mode in Android 2.3.4, the Google APIs add-on library includes the backported USB accessory APIs and they are contained in this namespace.
This namespace contains the classes that support USB accessory mode in Android 3.1.
The following classes support the USB accessory APIs:
Allows you to enumerate and communicate with connected USB accessories.
UsbManager manager = UsbManager.getInstance(this);
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
Represents a USB accessory and contains methods to access its identifying information.
UsbAccessory accessory = UsbManager.getAccessory(intent);
UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
Because the add-on library is a wrapper for the framework APIs, the classes that support the USB accessory feature are similar. You can use the reference documentation for the android.hardware.usb even if you are using the add-on library.
Not all Android-powered devices are guaranteed to support the USB accessory APIs.
To indicate that your application requires USB accessory support, add the following elements to your application’s manifest:
<uses-feature android:name="android.hardware.usb.accessory" />
<uses-sdk android:minSdkVersion="12" />
<uses-library android:name="com.android.future.usb.accessory" />
When users connect USB accessories to an Android-powered device, the Android system can determine whether your application is interested in the connected accessory. If so, you can set up communication with the accessory if desired.
To do this, your application has to:
Discover connected accessories by using an intent filter that filters for accessory attached events or by enumerating connected accessories and finding the appropriate one.
Ask the user for permission to communicate with the accessory, if not already obtained.
Communicate with the accessory by reading and writing data on the appropriate interface endpoints.
To have your application discover a particular USB device, you can specify an intent filter to filter for the android.hardware.usb.action.USB_ACCESSORY_ATTACHED intent.
In your activity, you can obtain the UsbAccessory that represents the attached device from the intent.
UsbAccessory accessory = UsbManager.getAccessory(intent);
UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
The following example shows how to declare the intent filter in your application’s manifest:
<activity ...> ... <intent-filter> <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" android:resource="@xml/accessory_filter" /> </activity>
The <meta-data> element points to an external XML resource file that declares identifying information about the device that you want to detect.
In the XML resource file, declare <usb-accessory> elements for the accessories that you want to filter.
<?xml version="1.0" encoding="utf-8"?> <resources> <usb-accessory model="DemoKit" manufacturer="Google" version="1.0"/> </resources>
Save the resource file in your application’s res/xml/ directory.
You can have your application enumerate accessories that have identified themselves while your application is running.
Use the UsbManager.getAccessoryList() method to get an array all the USB accessories that are connected:
UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE); UsbAccessory[] accessoryList = manager.getAcccessoryList();
|
|
Currently, only one connected accessory is supported at one time, but the API is designed to support multiple accessories in the future. |
Before communicating with the USB accessory, your application must have permission from your users.
|
|
If your application uses an intent filter to discover USB devices as they’re connected, it automatically receives permission if the user allows your application to handle the intent. If not, you must request permission explicitly in your application before connecting to the device. |
Explicitly asking for permission might be necessary in some situations such as when your application enumerates USB devices that are already connected and then wants to communicate with one.
To explicitly obtain permission:
Call UsbManager.requestPermission().
The call to requestPermission() displays a dialog to the user asking for permission to connect to the accessory.
The system generates a broadcast intent with a boolean EXTRA_PERMISSION_GRANTED extra indicating the user’s response.
The following sample code shows how to create a broadcast receiver to process the response:
private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"; private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { if (accessory != null) { // Call method to set up accessory communication } } else { Log.d(TAG, "permission denied for accessory " + accessory); } } } } };
To register the broadcast receiver, add this in your onCreate() method in your activity:
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); registerReceiver(mUsbReceiver, filter);
To display the dialog that asks users for permission to connect to the device, call the UsbManager.requestPermission() method:
UsbManager mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE); UsbAccessory accessory; // ... mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0); mUsbManager.requestPermission(accessory, mPermissionIntent);
You can communicate with the accessory by using the UsbManager to obtain a file descriptor that you can set up input and output streams to read and write data to descriptor.
UsbAccessory mAccessory; ParcelFileDescriptor mFileDescriptor; FileInputStream mInputStream; FileOutputStream mOutputStream; // ... private void openAccessory() { Log.d(TAG, "openAccessory: " + accessory); mFileDescriptor = mUsbManager.openAccessory(mAccessory); if (mFileDescriptor != null) { FileDescriptor fd = mFileDescriptor.getFileDescriptor(); mInputStream = new FileInputStream(fd); mOutputStream = new FileOutputStream(fd); Thread thread = new Thread(null, this, "AccessoryThread"); thread.start(); } }
In the thread’s run() method, you can read and write to the accessory by using the FileInputStream or FileOutputStream objects.
When reading data from an accessory with a FileInputStream object, ensure that the buffer that you use is big enough to store the USB packet data.
|
|
At a lower level, the packets are 64 bytes for USB full-speed accessories and 512 bytes for USB high-speed accessories. The Android accessory protocol bundles the packets together for both speeds into one logical packet for simplicity. |
When you are done communicating with an accessory or if the accessory was detached, close the file descriptor that you opened by calling close().
To listen for detached events, create a broadcast receiver like below:
BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) { UsbAccessory accessory = (UsbAccessory)intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY); if (accessory != null) { // call your method that cleans up and closes communication with the accessory } } } };
Creating the broadcast receiver within the application, and not the manifest, allows your application to handle detached events only while it is running.
The Android Open Accessory Development Kit (ADK) provides an implementation of an Android USB accessory that is based on the Arduino open source electronics prototyping platform.
The main hardware and software components of the ADK include:
INFORMATION IN THIS DOCUMENT IS PROVIDED “AS IS”. NO LICENSE, EXPRESS OR IMPLIED, BY ESTOPPEL OR OTHERWISE, TO ANY INTELLECTUAL PROPERTY RIGHTS IS GRANTED BY THIS DOCUMENT. MARAKANA ASSUMES NO LIABILITY WHATSOEVER AND MARAKANA DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY, RELATING TO THIS INFORMATION INCLUDING LIABILITY OR WARRANTIES RELATING TO FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR INFRINGEMENT OF ANY PATENT, COPYRIGHT OR OTHER INTELLECTUAL PROPERTY RIGHT.
Other names and brands may be claimed as the property of others.
Copyright © 2012 Marakana Inc.
/
#